├── .github └── dependabot.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── RECIPES.md ├── assets └── wooden_lounge_1k.tdl ├── examples ├── bevy │ ├── console.rs │ └── main.rs └── playground │ ├── main.rs │ └── nsi_render.rs ├── gapcD.jpg ├── osl ├── dlPrincipled.oso └── environmentLight.oso ├── polyhedron.jpg ├── rust-toolchain.toml ├── rustfmt.toml └── src ├── base_polyhedra.rs ├── grammar.pest ├── helpers.rs ├── io ├── bevy.rs ├── mod.rs ├── nsi.rs └── obj.rs ├── lib.rs ├── mesh_buffers.rs ├── operators.rs ├── parser.rs ├── selection.rs ├── tests.rs └── text_helpers.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" # Location of package manifests 8 | schedule: 9 | interval: "daily" 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /target 3 | Cargo.lock 4 | .DS_Store 5 | *.obj -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "polyhedron-ops" 3 | version = "0.2.9" 4 | authors = ["Moritz Moeller "] 5 | edition = "2021" 6 | keywords = ["3d", "creative", "geometry", "graphics", "rendering"] 7 | categories = ["graphics", "mathematics", "multimedia", "rendering"] 8 | license = "MIT OR Apache-2.0 OR Zlib" 9 | description = "Conway/Hart Polyhedron Operations" 10 | readme = "README.md" 11 | repository = "https://github.com/virtualritz/polyhedron-ops/" 12 | documentation = "https://docs.rs/polyhedron-ops/" 13 | 14 | [features] 15 | default = [] 16 | # Add support for reading/writing a mesh out as a Wavefront OBJ. 17 | obj = ["tobj"] 18 | # Add support for parsing Conway notation strings and turning them back into polyhedra. 19 | parser = ["pest", "pest_derive"] 20 | nsi = ["nsi-core", "bytemuck"] 21 | bevy = ["dep:bevy", "bevy_panorbit_camera"] 22 | tilings = [] 23 | console = ["bevy", "parser", "bevy_console", "clap"] 24 | 25 | [dependencies] 26 | # Add support to convert a Polyhedron into a bevy Mesh. 27 | bevy = { version = "0.14", default-features = false, features = [ 28 | "bevy_pbr", 29 | ], optional = true } 30 | bevy_console = { version = "0.12", optional = true } 31 | bevy_panorbit_camera = { version = "0.19", optional = true } 32 | bytemuck = { version = "1.14", features = [ 33 | "extern_crate_alloc", 34 | ], optional = true } 35 | clap = { version = "4.5", optional = true } 36 | itertools = "0.13" 37 | # Add support to render polyhedra with NSI. 38 | nsi-core = { version = "0.8", optional = true } 39 | num-traits = "0.2" 40 | pest = { version = "2.7", features = ["pretty-print"], optional = true } 41 | pest_derive = { version = "2.7", optional = true } 42 | rayon = "1" 43 | tobj = { version = "4", optional = true } 44 | ultraviolet = { version = "0.9", features = ["f64"] } 45 | 46 | [dev-dependencies] 47 | kiss3d = { version = "0.35", features = ["vertex_index_u32"] } 48 | bytemuck = "1.14.3" 49 | slice-of-array = "0.3" 50 | 51 | [target.'cfg(target_os = "linux")'.dependencies.bevy] 52 | version = "*" 53 | features = ["x11", "wayland"] 54 | 55 | [profile.dev] 56 | opt-level = 1 57 | 58 | [profile.dev.package."*"] 59 | opt-level = 3 60 | codegen-units = 1 61 | 62 | [profile.release] 63 | codegen-units = 1 64 | lto = "thin" 65 | 66 | [profile.wasm-release] 67 | inherits = "release" 68 | opt-level = "s" 69 | strip = "debuginfo" 70 | 71 | [[example]] 72 | name = "playground" 73 | required-features = ["obj"] 74 | 75 | [[example]] 76 | name = "bevy" 77 | required-features = ["bevy"] 78 | 79 | [package.metadata.docs.rs] 80 | features = ["nsi", "obj", "parser"] 81 | 82 | [patch.crates-io] 83 | kiss3d = { git = "https://github.com/sebcrozet/kiss3d.git", branch = "master" } 84 | #nsi = { git = "https://github.com/virtualritz/nsi.git", branch = "master" } 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Moritz Moeller 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polyhedron Operators 2 | 3 | This crate implements the [Conway Polyhedron 4 | Operators](http://en.wikipedia.org/wiki/Conway_polyhedron_notation) 5 | and their extensions by [George W. Hart](http://www.georgehart.com/) 6 | and others. 7 | 8 | ![Some brutalist Polyhedron, rendered with 3Delight|ɴsɪ](polyhedron.jpg) 9 | 10 | *Some brutalist polyhedron; rendered with 11 | [3Delight|ɴsɪ](https://www.3delight.com) and post processed in 12 | [Ansel](https://ansel.photos/).* 13 | 14 | This is an experiment to improve my understanding of iterators in Rust. 15 | It is based on [Kit Wallace](http://kitwallace.tumblr.com/tagged/conway)’s 16 | OpenSCAD code. As OpenSCAD Language is functional it lends itself well to 17 | translation into functional Rust. 18 | 19 | ```rust 20 | use polyhedron_ops::Polyhedron; 21 | use std::path::Path; 22 | 23 | // Conway notation: gapcD 24 | let polyhedron = 25 | Polyhedron::dodecahedron() // D 26 | .chamfer(None, true) // c 27 | .propellor(None, true) // p 28 | .ambo(None, true) // a 29 | .gyro(None, None, true) // g 30 | .finalize(); 31 | 32 | // Export as ./polyhedron-gapcD.obj 33 | polyhedron.write_obj(&Path::new("."), false); 34 | ``` 35 | 36 | The above code starts from a 37 | [dodecahedron](https://en.wikipedia.org/wiki/Dodecahedron) and 38 | iteratively applies four operators. 39 | 40 | The resulting shape is shown below. 41 | 42 | ![A polyhedron](gapcD.jpg) 43 | 44 | ## Caveat 45 | 46 | This is in a semi-polised shape. Documentation could be better (open an issue 47 | if you feel something is particualrly lacking). 48 | 49 | In short: use at your own risk. 50 | 51 | ## Cargo Features 52 | 53 | * `bevy` – Adds support for converting a polyhedron into a 54 | [`bevy`](https://bevyengine.org/) 55 | [`Mesh`](https://docs.rs/bevy/latest/bevy/render/mesh/struct.Mesh.html). 56 | 57 | * `nsi` – Adds support for sending a polyhedron to an offline renderer 58 | via the [ɴsɪ](https://crates.io/crates/nsi/) crate. 59 | 60 | * `obj` – Adds support for writing data out as 61 | [Wavefront OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file). 62 | 63 | * `parser` – Add support for parsing strings in 64 | [Conway Polyhedron Notation](https://en.wikipedia.org/wiki/Conway_polyhedron_notation). 65 | This feature implements `Polyhedron::TryFrom<&str>`. 66 | 67 | ## Base Shapes 68 | 69 | * [x] Platonic solids 70 | * [x] Prisms 71 | * [x] Antiprisms 72 | * [ ] Pyramids 73 | * [ ] Johnson Solids 74 | 75 | ## Supported Operators 76 | 77 | * [x] **a** – ambo 78 | * [x] **b** – bevel (equiv. to **ta**) 79 | * [x] **c** – chamfer 80 | * [x] **d** – dual 81 | * [x] **e** – expand (a.k.a. explode, equiv. to **aa**) 82 | * [x] **g** – gyro 83 | * [x] **i** – inset/loft (equiv. to **x,N**) 84 | * [x] **j** – join (equiv. to **dad**) 85 | * [x] **K** – Quick & dirty canonicalization 86 | * [x] **k** – kis 87 | * [x] **M** – medial (equiv. to **dta**) 88 | * [x] **m** – meta (equiv. to **k,,3j**) 89 | * [x] **n** – needle (equiv. to **dt**) 90 | * [x] **o** – ortho (equiv. to **jj**) 91 | * [x] **p** – propellor 92 | * [x] **q** – quinto 93 | * [x] **r** – reflect 94 | * [x] **S** – spherize 95 | * [x] **s** – snub (equiv. to **dgd**) 96 | * [x] **t** – truncate (equiv. to **dkd**) 97 | * [x] **v** – subdivide (Catmull-Clark) 98 | * [x] **w** – whirl 99 | * [x] **x** – extrude 100 | * [x] **z** – zip (equiv. to **dk**) 101 | 102 | ### Other Operators 103 | 104 | * [ ] **H** – hollow (called ‘intrude’ in Wings3D) 105 | * [ ] **h** – hexpropellor 106 | * [ ] **l** – stellate 107 | * [ ] **?** – triangulate 108 | 109 | ## Playing 110 | 111 | There is a playground example app to test things & have fun: 112 | 113 | ```text 114 | cargo run --release --example playground --features obj 115 | ``` 116 | 117 | If you want to produce images like the ones above you need to 118 | [download the free version of the 3Delight renderer](https://www.3delight.com/download) 119 | and install that. After that, run the example with 120 | [ɴsɪ](https://crates.io/crates/nsi/) support: 121 | 122 | ```text 123 | cargo run --release --example playground --features nsi,obj 124 | ``` 125 | 126 | ### Keyboard Commands 127 | 128 | Use keys matching the operator name from the above list to apply. 129 | 130 | Use `Up` and `Down` to adjust the parameter of the the last operator. 131 | Combine with `Shift` for 10× the change. 132 | 133 | `Delete` undoes the last (and only the last) operation. 134 | 135 | Press `F1` to render with 3Delight (requires a [3Delight|ɴsɪ 136 | installation](https://www.3delight.com/download)). 137 | Combine with `Shift` to render with 3Delight Cloud (requires registration). 138 | 139 | Press `Space` to save as `$HOME/polyhedron-.obj`. 140 | 141 | Export & render will always yield a correct OBJ though. Which you can 142 | view in Wings, Blender or another DCC app. 143 | 144 | ## Contributors 145 | 146 | [donbright](https://github.com/donbright) 147 | -------------------------------------------------------------------------------- /RECIPES.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | This is a list of recipes to create certain useful shapes with the operators in this crate. 4 | 5 | # Hexagon Sphere 6 | 7 | This is a bit of a misnomer since the resulting sphere approximation will also contain some pentagons. 8 | * `dodecahedron` 9 | * `kis` 10 | * `dual` 11 | 12 | Repeat the `kis`/`dual` combo any number of times to tessellate the shape into more hexagons. 13 | 14 | 15 | Mx0.30in0.20D -------------------------------------------------------------------------------- /assets/wooden_lounge_1k.tdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualritz/polyhedron-ops/537ea0f0973be1cfb77316008b7f0002aeac2016/assets/wooden_lounge_1k.tdl -------------------------------------------------------------------------------- /examples/bevy/console.rs: -------------------------------------------------------------------------------- 1 | use crate::RootPolyhedron; 2 | use bevy::prelude::{error, info, Assets, Handle, Mesh, Query, ResMut, With}; 3 | use bevy_console::ConsoleCommand; 4 | use clap::Parser; 5 | use polyhedron_ops::Polyhedron; 6 | use std::{error::Error, mem::replace}; 7 | 8 | pub mod prelude { 9 | pub use crate::console::{render_command, RenderCommand}; 10 | pub use bevy_console::{AddConsoleCommand, ConsolePlugin}; 11 | } 12 | 13 | fn render(conway: String) -> Result> { 14 | let polyhedron = Polyhedron::try_from(conway.as_str())? 15 | .normalize() 16 | .finalize(); 17 | Ok(polyhedron) 18 | } 19 | 20 | #[derive(Parser, ConsoleCommand)] 21 | #[command(name = "render")] 22 | pub struct RenderCommand { 23 | conway: String, 24 | } 25 | 26 | pub fn render_command( 27 | mesh_query: Query<&Handle, With>, 28 | mut meshes: ResMut>, 29 | mut log: ConsoleCommand, 30 | ) { 31 | if let Some(Ok(RenderCommand { conway })) = log.take() { 32 | let update = || -> Result> { 33 | let polyhedron = render(conway)?; 34 | let mesh_handle = mesh_query.get_single()?; 35 | let mesh = meshes 36 | .get_mut(mesh_handle) 37 | .ok_or("Root polyhedron mesh not found")?; 38 | let name = polyhedron.name().clone(); 39 | let _ = replace::(mesh, Mesh::from(polyhedron)); 40 | Ok(name) 41 | }; 42 | 43 | match update() { 44 | Ok(name) => { 45 | info!("Rendered polyhedron: {name}") 46 | } 47 | Err(e) => error!("Unable to render polyhedron: {e:?}"), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/bevy/main.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | app::{App, Startup}, 3 | asset::Assets, 4 | color::Color, 5 | core_pipeline::core_3d::Camera3dBundle, 6 | ecs::system::{Commands, ResMut}, 7 | math::Vec3, 8 | pbr::{DirectionalLightBundle, PbrBundle, StandardMaterial}, 9 | prelude::Component, 10 | render::{mesh::Mesh, view::Msaa}, 11 | transform::components::Transform, 12 | utils::default, 13 | DefaultPlugins, 14 | }; 15 | use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; 16 | use polyhedron_ops::Polyhedron; 17 | 18 | #[cfg(feature = "console")] 19 | mod console; 20 | #[cfg(feature = "console")] 21 | use console::prelude::*; 22 | 23 | #[derive(Component)] 24 | pub struct RootPolyhedron; 25 | 26 | fn main() { 27 | let mut app = App::new(); 28 | 29 | app.insert_resource(Msaa::Sample4) 30 | .add_plugins(DefaultPlugins) 31 | .add_plugins(PanOrbitCameraPlugin) 32 | .add_systems(Startup, setup); 33 | 34 | #[cfg(feature = "console")] 35 | app.add_plugins(ConsolePlugin) 36 | .add_console_command::(render_command); 37 | 38 | app.run(); 39 | } 40 | 41 | fn setup( 42 | mut commands: Commands, 43 | mut meshes: ResMut>, 44 | mut materials: ResMut>, 45 | ) { 46 | // chamfered_tetrahedron 47 | let polyhedron = Polyhedron::dodecahedron() // D 48 | .bevel(None, None, None, None, true) // b 49 | .normalize() 50 | .finalize(); 51 | 52 | commands.spawn(( 53 | PbrBundle { 54 | mesh: meshes.add(Mesh::from(polyhedron)), 55 | material: materials.add(Color::srgb(0.4, 0.35, 0.3)), 56 | ..Default::default() 57 | }, 58 | RootPolyhedron, 59 | )); 60 | 61 | // Light. 62 | commands.spawn(DirectionalLightBundle { 63 | transform: Transform::from_translation(Vec3::new(4.0, 8.0, 4.0)), 64 | ..Default::default() 65 | }); 66 | 67 | // Camera. 68 | commands.spawn(( 69 | Camera3dBundle { 70 | transform: Transform::from_translation(Vec3::new(-3.0, 3.0, 5.0)), 71 | ..default() 72 | }, 73 | PanOrbitCamera::default(), 74 | )); 75 | } 76 | -------------------------------------------------------------------------------- /examples/playground/main.rs: -------------------------------------------------------------------------------- 1 | use kiss3d::{ 2 | camera::{ArcBall, FirstPerson}, 3 | event::{Action, Key, Modifiers, WindowEvent}, 4 | light::Light, 5 | nalgebra as na, 6 | resource::Mesh, 7 | window::Window, 8 | }; 9 | use na::{Point3, Vector3}; 10 | use polyhedron_ops::*; 11 | use rayon::prelude::*; 12 | use std::{ 13 | cell::RefCell, env, env::current_dir, error::Error, io, io::Write, 14 | path::Path, rc::Rc, 15 | }; 16 | 17 | #[cfg(feature = "nsi")] 18 | mod nsi_render; 19 | 20 | #[cfg(feature = "nsi")] 21 | use slice_of_array::prelude::*; 22 | 23 | #[cfg(feature = "nsi")] 24 | use kiss3d::camera::Camera; 25 | 26 | use itertools::Itertools; 27 | 28 | #[derive(PartialEq)] 29 | pub enum RenderType { 30 | Normal, 31 | Cloud, 32 | Dump, 33 | } 34 | 35 | fn into_mesh(polyhedron: Polyhedron) -> Mesh { 36 | let (face_index, points, normals) = polyhedron.to_triangle_mesh_buffers(); 37 | 38 | Mesh::new( 39 | // Duplicate points per face so we can 40 | // match the normals per face. 41 | points 42 | .par_iter() 43 | .map(|p| na::Point3::::new(p.x, p.y, p.z)) 44 | .collect::>(), 45 | face_index 46 | .iter() 47 | .tuples::<(_, _, _)>() 48 | .map(|i| na::Point3::::new(*i.0, *i.1, *i.2)) 49 | .collect::>(), 50 | Some( 51 | normals 52 | .par_iter() 53 | .map(|n| na::Vector3::new(n.x, n.y, n.z)) 54 | .collect::>(), 55 | ), 56 | None, 57 | false, 58 | ) 59 | } 60 | 61 | fn main() -> Result<(), Box> { 62 | let args: Vec = env::args().collect(); 63 | 64 | let mut poly = if args.len() > 1 { 65 | Polyhedron::read_obj(&Path::new(&args[1]), true)? 66 | } else { 67 | Polyhedron::tetrahedron() 68 | }; 69 | 70 | poly.normalize(); 71 | 72 | let distance = 2.0f32; 73 | let eye = Point3::new(distance, distance, distance); 74 | let at = Point3::origin(); 75 | let mut first_person = FirstPerson::new(eye, at); 76 | let mut arc_ball = ArcBall::new(eye, at); 77 | let mut use_arc_ball = true; 78 | 79 | let mut window = Window::new("Polyhedron Operations"); 80 | window.set_light(Light::StickToCamera); 81 | 82 | let mesh = Rc::new(RefCell::new(into_mesh(poly.clone()))); 83 | let mut c = window.add_mesh(mesh, Vector3::new(1.0, 1.0, 1.0)); 84 | 85 | c.set_color(0.9, 0.8, 0.7); 86 | c.enable_backface_culling(false); 87 | c.set_points_size(10.); 88 | 89 | window.set_light(Light::StickToCamera); 90 | window.set_framerate_limit(Some(60)); 91 | 92 | let mut last_op = 'n'; 93 | let mut last_op_value = 0.; 94 | let mut alter_last_op = false; 95 | let mut last_poly = poly.clone(); 96 | 97 | let path = current_dir()?; 98 | let mut render_quality = 0; 99 | 100 | let mut turntable = false; 101 | 102 | println!( 103 | "Press one of:\n\ 104 | ____________________________________________ Start Shapes (Reset) ______\n\ 105 | [T]etrahedron [P]rism ↑↓\n\ 106 | [C]ube (hexahedron)\n\ 107 | [O]ctahedron\n\ 108 | [D]dodecahedron\n\ 109 | [I]cosehedron [G]eometry (if loaded)\n\ 110 | ______________________________________________________ Operations ______\n\ 111 | [a]mbo ↑↓\n\ 112 | [b]evel ↑↓\n\ 113 | [c]chamfer ↑↓\n\ 114 | [d]ual\n\ 115 | [e]xpand ↑↓\n\ 116 | [g]yro ↑↓\n\ 117 | [i]nset ↑↓\n\ 118 | [j]oin ↑↓\n\ 119 | Quic[K] & dirty canonicalization\n\ 120 | [k]iss ↑↓\n\ 121 | [M]edial ↑↓\n\ 122 | [m]eta ↑↓\n\ 123 | [n]eedle ↑↓\n\ 124 | [o]rtho ↑↓\n\ 125 | [p]propeller ↑↓\n\ 126 | [q]uinto ↑↓\n\ 127 | [r]eflect\n\ 128 | [s]nub ↑↓\n\ 129 | [S]pherize ↑↓\n\ 130 | [t]runcate ↑↓\n\ 131 | Catmull-Clark subdi[v]ide\n\ 132 | [w]hirl ↑↓\n\ 133 | e[x]trude ↑↓\n\ 134 | [z]ip ↑↓\n\ 135 | _______________________________________________________ Modifiers ______\n\ 136 | (Shift)+⬆⬇︎ – modify the last operation marked with ↑↓ (10× w. [Shift])\n\ 137 | [Delete] – Undo last operation\n\ 138 | _______________________________________________________ Exporting ______" 139 | ); 140 | #[cfg(feature = "nsi")] 141 | print!("([Shift])+"); 142 | print!("[Space] – save as OBJ"); 143 | #[cfg(feature = "nsi")] 144 | print!( 145 | " (dump to NSI w. [Shift])\n\ 146 | _______________________________________________________ Rendering ______\n\ 147 | (Shift)+[F1] – Render (in the cloud w. [Shift])\n\ 148 | [0]..[9] – Set render quality: [preview]..[super high quality]\n\ 149 | [Ctrl]+[T] – Toggle turntable rendering (72 frames)" 150 | ); 151 | print!( 152 | "\n________________________________________________________________________\n\ 153 | ❯ {} – render quality {} {:<80}\r", 154 | poly.name(), 155 | render_quality, 156 | if turntable { "(turntable)" } else { "" }, 157 | ); 158 | io::stdout().flush().unwrap(); 159 | 160 | while !window.should_close() { 161 | // rotate the arc-ball camera. 162 | let curr_yaw = arc_ball.yaw(); 163 | arc_ball.set_yaw(curr_yaw + 0.01); 164 | 165 | // update the current camera. 166 | for event in window.events().iter() { 167 | if let WindowEvent::Key(key, Action::Release, modifiers) = 168 | event.value 169 | { 170 | match key { 171 | Key::Numpad1 => use_arc_ball = true, 172 | Key::Numpad2 => use_arc_ball = false, 173 | Key::Key0 => render_quality = 0, 174 | Key::Key1 => { 175 | render_quality = 1; 176 | } 177 | Key::Key2 => { 178 | render_quality = 2; 179 | } 180 | Key::Key3 => { 181 | render_quality = 3; 182 | } 183 | Key::Key4 => { 184 | render_quality = 4; 185 | } 186 | Key::Key5 => { 187 | render_quality = 5; 188 | } 189 | Key::Key6 => { 190 | render_quality = 6; 191 | } 192 | Key::Key7 => { 193 | render_quality = 7; 194 | } 195 | Key::Key8 => { 196 | render_quality = 8; 197 | } 198 | Key::Key9 => { 199 | render_quality = 9; 200 | } 201 | Key::A => { 202 | alter_last_op = false; 203 | last_poly = poly.clone(); 204 | last_op_value = 0.5; 205 | poly.ambo(None, true); 206 | poly.normalize(); 207 | last_op = 'a'; 208 | } 209 | Key::B => { 210 | alter_last_op = false; 211 | last_poly = poly.clone(); 212 | last_op_value = 0.; 213 | poly.bevel(None, None, None, None, true); 214 | poly.normalize(); 215 | last_op = 'b'; 216 | } 217 | Key::C => { 218 | alter_last_op = false; 219 | last_poly = poly.clone(); 220 | if modifiers.intersects(Modifiers::Shift) { 221 | poly = Polyhedron::hexahedron(); 222 | poly.normalize(); 223 | } else { 224 | last_op_value = 0.5; 225 | poly.chamfer(None, true); 226 | poly.normalize(); 227 | last_op = 'c'; 228 | } 229 | } 230 | Key::D => { 231 | alter_last_op = false; 232 | last_poly = poly.clone(); 233 | if modifiers.intersects(Modifiers::Shift) { 234 | poly = Polyhedron::dodecahedron(); 235 | } else { 236 | last_op_value = 0.; 237 | poly.dual(true); 238 | } 239 | poly.normalize(); 240 | last_op = '_'; 241 | } 242 | Key::E => { 243 | alter_last_op = false; 244 | last_poly = poly.clone(); 245 | last_op_value = 0.5; 246 | poly.expand(None, true); 247 | poly.normalize(); 248 | last_op = 'e'; 249 | } 250 | Key::G => { 251 | if modifiers.intersects(Modifiers::Shift) { 252 | poly = Polyhedron::read_obj( 253 | &Path::new(&args[1]), 254 | true, 255 | )?; 256 | poly.normalize(); 257 | } else { 258 | alter_last_op = false; 259 | last_poly = poly.clone(); 260 | last_op_value = 0.; 261 | poly.gyro(None, None, true); 262 | poly.normalize(); 263 | last_op = 'g'; 264 | } 265 | } 266 | Key::I => { 267 | alter_last_op = false; 268 | last_poly = poly.clone(); 269 | if modifiers.intersects(Modifiers::Shift) { 270 | poly = Polyhedron::icosahedron(); 271 | } else { 272 | alter_last_op = false; 273 | last_poly = poly.clone(); 274 | last_op_value = 0.3; 275 | poly.inset(None, None, true); 276 | last_op = 'i'; 277 | } 278 | poly.normalize(); 279 | } 280 | Key::J => { 281 | alter_last_op = false; 282 | last_poly = poly.clone(); 283 | last_op_value = 0.5; 284 | poly.join(None, true); 285 | poly.normalize(); 286 | last_op = 'j'; 287 | } 288 | Key::K => { 289 | alter_last_op = false; 290 | last_poly = poly.clone(); 291 | last_op_value = 0.; 292 | if modifiers.intersects(Modifiers::Shift) { 293 | poly.planarize(Some(100), true); 294 | last_op = 'K'; 295 | } else { 296 | poly.kis(None, None, None, None, true); 297 | last_op = 'k'; 298 | } 299 | poly.normalize(); 300 | } 301 | Key::M => { 302 | alter_last_op = false; 303 | last_poly = poly.clone(); 304 | last_op_value = 0.; 305 | if modifiers.intersects(Modifiers::Shift) { 306 | poly.medial(None, None, None, None, true); 307 | last_op = 'M'; 308 | } else { 309 | poly.meta(None, None, None, None, true); 310 | last_op = 'm'; 311 | } 312 | poly.normalize(); 313 | } 314 | Key::N => { 315 | alter_last_op = false; 316 | last_poly = poly.clone(); 317 | last_op_value = 0.; 318 | /*if modifiers.intersects(Modifiers::Shift) { 319 | //poly.canonicalize(Some(1), true); 320 | poly.normalize(); 321 | last_op = 'N'; 322 | } else*/ 323 | { 324 | poly.needle(None, None, None, true); 325 | poly.normalize(); 326 | last_op = 'n'; 327 | } 328 | } 329 | Key::O => { 330 | alter_last_op = false; 331 | last_poly = poly.clone(); 332 | if modifiers.intersects(Modifiers::Shift) { 333 | poly = Polyhedron::octahedron(); 334 | poly.normalize(); 335 | } else { 336 | last_op_value = 0.5; 337 | poly.ortho(None, true); 338 | poly.normalize(); 339 | last_op = 'o'; 340 | } 341 | } 342 | Key::P => { 343 | alter_last_op = false; 344 | last_poly = poly.clone(); 345 | if modifiers.intersects(Modifiers::Shift) { 346 | last_op_value = 0.03; 347 | poly = Polyhedron::prism(None); 348 | poly.normalize(); 349 | last_op = 'P'; 350 | } else { 351 | last_op_value = 1. / 3.; 352 | poly.propellor(None, true); 353 | poly.normalize(); 354 | last_op = 'p'; 355 | } 356 | } 357 | Key::Q => { 358 | alter_last_op = false; 359 | last_poly = poly.clone(); 360 | last_op_value = 0.5; 361 | poly.quinto(None, true); 362 | poly.normalize(); 363 | last_op = 'q'; 364 | } 365 | Key::R => { 366 | alter_last_op = false; 367 | last_poly = poly.clone(); 368 | poly.reflect(true); 369 | poly.normalize(); 370 | last_op = '_'; 371 | } 372 | Key::S => { 373 | alter_last_op = false; 374 | last_poly = poly.clone(); 375 | if modifiers.intersects(Modifiers::Shift) { 376 | last_op_value = 1.0; 377 | poly.spherize(None, true); 378 | last_op = 'S'; 379 | } else { 380 | last_op_value = 0.; 381 | poly.snub(None, None, true); 382 | poly.normalize(); 383 | last_op = 's'; 384 | } 385 | } 386 | Key::T => { 387 | if modifiers.intersects(Modifiers::Shift) { 388 | alter_last_op = false; 389 | last_poly = poly.clone(); 390 | poly = Polyhedron::tetrahedron(); 391 | poly.normalize(); 392 | } else if modifiers.intersects(Modifiers::Control) { 393 | turntable = !turntable; 394 | } else { 395 | alter_last_op = false; 396 | last_poly = poly.clone(); 397 | last_op_value = 0.; 398 | poly.truncate(None, None, None, true); 399 | poly.normalize(); 400 | last_op = 't'; 401 | } 402 | } 403 | Key::V => { 404 | alter_last_op = false; 405 | last_poly = poly.clone(); 406 | last_op_value = 0.; 407 | poly.catmull_clark_subdivide(true); 408 | poly.normalize(); 409 | last_op = 'v'; 410 | } 411 | Key::W => { 412 | alter_last_op = false; 413 | last_poly = poly.clone(); 414 | last_op_value = 0.; 415 | poly.whirl(None, None, true); 416 | poly.normalize(); 417 | last_op = 'w'; 418 | } 419 | Key::X => { 420 | alter_last_op = false; 421 | last_poly = poly.clone(); 422 | last_op_value = 0.3; 423 | poly.extrude(None, None, None, true); 424 | poly.normalize(); 425 | last_op = 'x'; 426 | } 427 | Key::Z => { 428 | alter_last_op = false; 429 | last_poly = poly.clone(); 430 | last_op_value = 0.; 431 | poly.zip(None, None, None, true); 432 | poly.normalize(); 433 | last_op = 'z'; 434 | } 435 | Key::Space => { 436 | if modifiers.intersects(Modifiers::Shift) { 437 | #[cfg(feature = "nsi")] 438 | { 439 | let xform = arc_ball 440 | .inverse_transformation() 441 | .iter() 442 | .map(|e| *e as f64) 443 | .collect::>(); 444 | 445 | println!( 446 | "Dumped to {}", 447 | nsi_render::nsi_render( 448 | &path, 449 | &poly, 450 | xform.as_array(), 451 | render_quality, 452 | RenderType::Dump, 453 | turntable, 454 | ) 455 | ); 456 | } 457 | } else { 458 | println!( 459 | "Exported to {}", 460 | poly.write_obj(&path, true).unwrap().display() 461 | ); 462 | } 463 | } 464 | Key::Up => { 465 | alter_last_op = true; 466 | if modifiers.intersects(Modifiers::Shift) { 467 | last_op_value += 0.1; 468 | } else { 469 | last_op_value += 0.01; 470 | } 471 | } 472 | Key::Down => { 473 | alter_last_op = true; 474 | if modifiers.intersects(Modifiers::Shift) { 475 | last_op_value -= 0.1; 476 | } else { 477 | last_op_value -= 0.01; 478 | } 479 | } 480 | Key::Delete => { 481 | poly = last_poly.clone(); 482 | } 483 | #[cfg(feature = "nsi")] 484 | Key::F1 => { 485 | let xform = arc_ball 486 | .inverse_transformation() 487 | .iter() 488 | .map(|e| *e as f64) 489 | .collect::>(); 490 | 491 | nsi_render::nsi_render( 492 | Path::new(""), 493 | &poly, 494 | xform.as_array(), 495 | render_quality, 496 | if modifiers.intersects(Modifiers::Shift) { 497 | RenderType::Cloud 498 | } else { 499 | RenderType::Normal 500 | }, 501 | turntable, 502 | ); 503 | } 504 | _ => { 505 | break; 506 | } 507 | } 508 | if alter_last_op { 509 | alter_last_op = false; 510 | if '_' != last_op { 511 | poly = last_poly.clone(); 512 | } 513 | match last_op { 514 | 'a' => { 515 | poly.ambo(Some(last_op_value), true); 516 | } 517 | 'b' => { 518 | poly.bevel( 519 | Some(last_op_value), 520 | Some(last_op_value), 521 | None, 522 | None, 523 | true, 524 | ); 525 | } 526 | 'c' => { 527 | poly.chamfer(Some(last_op_value), true); 528 | } 529 | 'e' => { 530 | poly.expand(Some(last_op_value), true); 531 | } 532 | 'g' => { 533 | poly.gyro(None, Some(last_op_value), true); 534 | } 535 | 'i' => { 536 | poly.inset(Some(last_op_value), None, true); 537 | } 538 | 'j' => { 539 | poly.join(Some(last_op_value), true); 540 | } 541 | 'k' => { 542 | poly.kis( 543 | Some(last_op_value), 544 | None, 545 | None, 546 | None, 547 | true, 548 | ); 549 | } 550 | 'm' => { 551 | poly.meta( 552 | Some(last_op_value), 553 | Some(last_op_value), 554 | None, 555 | None, 556 | true, 557 | ); 558 | } 559 | 'o' => { 560 | poly.ortho(Some(last_op_value), true); 561 | } 562 | 'p' => { 563 | poly.propellor(Some(last_op_value), true); 564 | } 565 | 'P' => { 566 | poly = Polyhedron::prism(Some( 567 | (last_op_value * 100.) as _, 568 | )); 569 | poly.normalize(); 570 | } 571 | 'q' => { 572 | poly.quinto(Some(last_op_value), true); 573 | } 574 | 'M' => { 575 | poly.medial( 576 | Some(last_op_value), 577 | Some(last_op_value), 578 | None, 579 | None, 580 | true, 581 | ); 582 | } 583 | 'n' => { 584 | poly.needle(Some(last_op_value), None, None, true); 585 | } 586 | 's' => { 587 | poly.snub(None, Some(last_op_value), true); 588 | } 589 | 'S' => { 590 | poly.spherize(Some(last_op_value), true); 591 | } 592 | 't' => { 593 | poly.truncate( 594 | Some(last_op_value), 595 | None, 596 | None, 597 | true, 598 | ); 599 | } 600 | 'w' => { 601 | poly.whirl(None, Some(last_op_value), true); 602 | } 603 | 'x' => { 604 | poly.extrude(Some(last_op_value), None, None, true); 605 | } 606 | 'z' => { 607 | poly.zip(Some(last_op_value), None, None, true); 608 | } 609 | _ => (), 610 | } 611 | if '_' != last_op { 612 | poly.normalize(); 613 | } 614 | } 615 | c.unlink(); 616 | let mesh = Rc::new(RefCell::new(into_mesh(poly.clone()))); 617 | c = window.add_mesh(mesh, Vector3::new(1.0, 1.0, 1.0)); 618 | c.set_color(0.9, 0.8, 0.7); 619 | c.enable_backface_culling(false); 620 | c.set_points_size(10.); 621 | 622 | print!( 623 | "❯ {} – render quality {} {:<80}\r", 624 | poly.name(), 625 | render_quality, 626 | if turntable { "(turntable)" } else { "" }, 627 | ); 628 | io::stdout().flush().unwrap(); 629 | } 630 | } 631 | 632 | window.draw_line( 633 | &Point3::origin(), 634 | &Point3::new(1.0, 0.0, 0.0), 635 | &Point3::new(1.0, 0.0, 0.0), 636 | ); 637 | window.draw_line( 638 | &Point3::origin(), 639 | &Point3::new(0.0, 1.0, 0.0), 640 | &Point3::new(0.0, 1.0, 0.0), 641 | ); 642 | window.draw_line( 643 | &Point3::origin(), 644 | &Point3::new(0.0, 0.0, 1.0), 645 | &Point3::new(0.0, 0.0, 1.0), 646 | ); 647 | 648 | if use_arc_ball { 649 | window.render_with_camera(&mut arc_ball); 650 | } else { 651 | window.render_with_camera(&mut first_person); 652 | } 653 | } 654 | 655 | Ok(()) 656 | } 657 | -------------------------------------------------------------------------------- /examples/playground/nsi_render.rs: -------------------------------------------------------------------------------- 1 | pub use crate::*; 2 | use nsi::*; 3 | use nsi_core as nsi; 4 | use std::f64::consts::TAU; 5 | use ultraviolet as uv; 6 | 7 | const FPS: u32 = 60; 8 | const TURNTABLE_SECONDS: u32 = 1; 9 | const FRAME_STEP: f64 = 10 | 360.0 / TURNTABLE_SECONDS as f64 / FPS as f64 * TAU / 90.0; 11 | 12 | /// Returns the name of the `screen` node that was created. 13 | fn nsi_globals_and_camera( 14 | c: &Context, 15 | name: &str, 16 | _camera_xform: &[f64; 16], 17 | render_quality: u32, 18 | turntable: bool, 19 | ) { 20 | // Setup a camera transform. 21 | c.create("camera_xform", TRANSFORM, None); 22 | c.connect("camera_xform", None, ROOT, "objects", None); 23 | 24 | c.set_attribute( 25 | "camera_xform", 26 | &[double_matrix!( 27 | "transformationmatrix", 28 | //camera_xform 29 | &[1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 5., 1.,] 30 | )], 31 | ); 32 | 33 | // Setup a camera. 34 | c.create("camera", PERSPECTIVE_CAMERA, None); 35 | 36 | c.set_attribute( 37 | "camera", 38 | &[ 39 | float!("fov", 35.), 40 | doubles!("shutterrange", &[0.0, 1.0]), //.array_len(2), 41 | doubles!("shutteropening", &[0.5, 0.5]), //.array_len(2) 42 | ], 43 | ); 44 | c.connect("camera", None, "camera_xform", "objects", None); 45 | 46 | // Setup a screen. 47 | c.create("screen", SCREEN, None); 48 | c.connect("screen", None, "camera", "screens", None); 49 | 50 | c.set_attribute( 51 | "screen", 52 | &[ 53 | integers!("resolution", &[512, 512]).array_len(2), 54 | integer!( 55 | "oversampling", 56 | if turntable { 57 | 1 << (3 + render_quality) 58 | } else { 59 | 32 60 | } 61 | ), 62 | ], 63 | ); 64 | 65 | c.set_attribute( 66 | ".global", 67 | &[ 68 | integer!("renderatlowpriority", 1), 69 | string!("bucketorder", "circle"), 70 | integer!("quality.shadingsamples", 1 << (3 + render_quality)), 71 | integer!("maximumraydepth.reflection", 6), 72 | ], 73 | ); 74 | 75 | c.create("albedo", OUTPUT_LAYER, None); 76 | c.set_attribute( 77 | "albedo", 78 | &[ 79 | string!("variablename", "albedo"), 80 | string!("variablesource", "shader"), 81 | string!("layertype", "color"), 82 | string!("scalarformat", "float"), 83 | string!("filter", "box"), 84 | double!("filterwidth", 1.), 85 | ], 86 | ); 87 | c.connect("albedo", None, "screen", "outputlayers", None); 88 | 89 | // Normal layer. 90 | c.create("normal", OUTPUT_LAYER, None); 91 | c.set_attribute( 92 | "normal", 93 | &[ 94 | string!("variablename", "N.world"), 95 | string!("variablesource", "builtin"), 96 | string!("layertype", "vector"), 97 | string!("scalarformat", "float"), 98 | string!("filter", "box"), 99 | double!("filterwidth", 1.), 100 | ], 101 | ); 102 | c.connect("normal", None, "screen", "outputlayers", None); 103 | 104 | // Setup an output layer. 105 | c.create(name, OUTPUT_LAYER, None); 106 | c.set_attribute( 107 | name, 108 | &[ 109 | string!("variablename", "Ci"), 110 | integer!("withalpha", 1), 111 | string!("scalarformat", "float"), 112 | double!("filterwidth", 1.), 113 | ], 114 | ); 115 | c.connect(name, None, "screen", "outputlayers", None); 116 | 117 | // Setup an output driver. 118 | c.create("driver", OUTPUT_DRIVER, None); 119 | c.connect("driver", None, name, "outputdrivers", None); 120 | c.set_attribute( 121 | "driver", 122 | &[ 123 | string!("drivername", "idisplay"), 124 | string!("imagefilename", name.to_string() + ".exr"), 125 | //string!("filename", name.to_string() + ".exr"), 126 | ], 127 | ); 128 | 129 | /* 130 | c.create("driver2", OUTPUT_DRIVER, None); 131 | c.connect("driver2", None, name, "outputdrivers", None); 132 | c.connect("driver2", None, "albedo", "outputdrivers", None); 133 | c.connect("driver2", None, "normal", "outputdrivers", None); 134 | c.set_attribute( 135 | "driver2", 136 | &[ 137 | string!("drivername", "r-display"), 138 | string!("imagefilename", name), 139 | float!("denoise", 1.), 140 | ], 141 | );*/ 142 | } 143 | 144 | fn nsi_environment(c: &Context) { 145 | // Set up an environment light. 146 | c.create("env_xform", TRANSFORM, None); 147 | c.connect("env_xform", None, ROOT, "objects", None); 148 | 149 | c.create("environment", ENVIRONMENT, None); 150 | c.connect("environment", None, "env_xform", "objects", None); 151 | 152 | c.create("env_attrib", ATTRIBUTES, None); 153 | c.connect( 154 | "env_attrib", 155 | None, 156 | "environment", 157 | "geometryattributes", 158 | None, 159 | ); 160 | 161 | c.set_attribute("env_attrib", &[integer!("visibility.camera", 0)]); 162 | 163 | c.create("env_shader", SHADER, None); 164 | c.connect("env_shader", None, "env_attrib", "surfaceshader", None); 165 | 166 | // Environment light attributes. 167 | c.set_attribute( 168 | "env_shader", 169 | &[ 170 | string!("shaderfilename", "${DELIGHT}/osl/environmentLight"), 171 | float!("intensity", 1.), 172 | ], 173 | ); 174 | 175 | c.set_attribute( 176 | "env_shader", 177 | &[string!("image", "assets/wooden_lounge_1k.tdl")], 178 | ); 179 | } 180 | 181 | fn nsi_material(c: &Context, name: &str) { 182 | // Particle attributes. 183 | let attribute_name = format!("{}_attrib", name); 184 | c.create(&attribute_name, ATTRIBUTES, None); 185 | c.connect(&attribute_name, None, name, "geometryattributes", None); 186 | 187 | // Particle shader. 188 | let shader_name = format!("{}_shader", name); 189 | c.create(&shader_name, SHADER, None); 190 | c.connect(&shader_name, None, &attribute_name, "surfaceshader", None); 191 | 192 | /* 193 | c.set_attribute( 194 | &shader_name, 195 | &[ 196 | string!( 197 | "shaderfilename", 198 | PathBuf::from(path) 199 | .join("osl") 200 | .join("dlPrincipled") 201 | .to_string_lossy() 202 | .into_owned() 203 | ), 204 | color!("i_color", &[1.0f32, 0.6, 0.3]), 205 | //arg!("coating_thickness", &0.1f32), 206 | float!("roughness", 0.3f32), 207 | float!("specular_level", 0.5f32), 208 | float!("metallic", 1.0f32), 209 | float!("anisotropy", 0.9f32), 210 | float!("thin_film_thickness", 0.6), 211 | float!("thin_film_ior", 3.0), 212 | //color!("incandescence", &[0.0f32, 0.0, 0.0]), 213 | ], 214 | ); 215 | 216 | 217 | c.set_attribute( 218 | &shader_name, 219 | &[ 220 | string!( 221 | "shaderfilename", 222 | PathBuf::from(path) 223 | .join("osl") 224 | .join("dlGlass") 225 | .to_string_lossy() 226 | .into_owned() 227 | ), 228 | float!("refract_roughness", 0.666f32), 229 | ], 230 | );*/ 231 | 232 | c.set_attribute( 233 | &shader_name, 234 | &[ 235 | string!("shaderfilename", "${DELIGHT}/osl/dlMetal"), 236 | color!("i_color", &[1.0f32, 0.6, 0.3]), 237 | float!("roughness", 0.3), 238 | //float!("anisotropy", 0.9f32), 239 | float!("thin_film_thickness", 0.6), 240 | float!("thin_film_ior", 3.0), 241 | ], 242 | ); 243 | } 244 | 245 | pub fn nsi_render( 246 | path: &Path, 247 | polyhedron: &crate::Polyhedron, 248 | camera_xform: &[f64; 16], 249 | render_quality: u32, 250 | render_type: crate::RenderType, 251 | turntable: bool, 252 | ) -> std::string::String { 253 | let destination = 254 | path.join(format!("polyhedron-{}.nsi", polyhedron.name())); 255 | 256 | let ctx = { 257 | match render_type { 258 | RenderType::Normal => Context::new(None), 259 | RenderType::Cloud => Context::new(Some(&[integer!("cloud", 1)])), 260 | RenderType::Dump => Context::new(Some(&[ 261 | string!("type", "apistream"), 262 | string!("streamfilename", destination.to_str().unwrap()), 263 | ])), 264 | } 265 | } 266 | .unwrap(); 267 | 268 | nsi_globals_and_camera( 269 | &ctx, 270 | polyhedron.name(), 271 | camera_xform, 272 | render_quality, 273 | turntable, 274 | ); 275 | 276 | nsi_environment(&ctx); 277 | 278 | let name = polyhedron.to_nsi( 279 | &ctx, 280 | Some(&(polyhedron.name().to_string() + "-mesh")), 281 | None, 282 | None, 283 | None, 284 | ); 285 | 286 | nsi_material(&ctx, &name); 287 | 288 | /* 289 | ctx.append( 290 | ROOT, 291 | None, 292 | ctx.append( 293 | &ctx.rotation(Some("mesh-rotation"), (frame * 5) as f64, &[0., 1., 0.]), 294 | None, 295 | &name, 296 | ) 297 | .0, 298 | );*/ 299 | 300 | if turntable { 301 | ctx.create("rotation", TRANSFORM, None); 302 | ctx.connect("rotation", None, ROOT, "objects", None); 303 | ctx.connect(&name, None, "rotation", "objects", None); 304 | 305 | for frame in 0..TURNTABLE_SECONDS * FPS { 306 | ctx.set_attribute( 307 | "driver", 308 | &[string!("filename", format!("{}_{:02}.exr", name, frame))], 309 | ); 310 | 311 | ctx.set_attribute_at_time( 312 | "rotation", 313 | 0.0, 314 | &[double_matrix!( 315 | "transformationmatrix", 316 | uv::DMat4::from_angle_plane( 317 | (frame as f64 * FRAME_STEP) as _, 318 | uv::DBivec3::from_normalized_axis(uv::DVec3::new( 319 | 0., 1., 0. 320 | )) 321 | ) 322 | .transposed() 323 | .as_array() 324 | )], 325 | ); 326 | 327 | ctx.set_attribute_at_time( 328 | "rotation", 329 | 1.0, 330 | &[double_matrix!( 331 | "transformationmatrix", 332 | uv::DMat4::from_angle_plane( 333 | ((frame + 1) as f64 * FRAME_STEP) as _, 334 | uv::DBivec3::from_normalized_axis(uv::DVec3::new( 335 | 0., 1., 0. 336 | )) 337 | ) 338 | .transposed() 339 | .as_array() 340 | )], 341 | ); 342 | 343 | ctx.render_control(nsi::Action::Synchronize, None); 344 | ctx.render_control(nsi::Action::Start, None); 345 | ctx.render_control(nsi::Action::Wait, None); 346 | } 347 | } else { 348 | ctx.connect(&name, None, ROOT, "objects", None); 349 | 350 | //if RenderType::Dump != render_type { 351 | ctx.render_control(nsi::Action::Start, None); 352 | //} 353 | } 354 | 355 | //if RenderType::Dump != render_type { 356 | ctx.render_control(nsi::Action::Wait, None); 357 | //} 358 | 359 | destination.to_string_lossy().to_string() 360 | } 361 | -------------------------------------------------------------------------------- /gapcD.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualritz/polyhedron-ops/537ea0f0973be1cfb77316008b7f0002aeac2016/gapcD.jpg -------------------------------------------------------------------------------- /osl/environmentLight.oso: -------------------------------------------------------------------------------- 1 | OpenShadingLanguage 1.00 2 | # Compiled by oslc 1.11.3 3 | # options: -q -o build/environmentLight.oso 4 | surface environmentLight %meta{string[1],tags,"environment"} 5 | param string image "" %meta{string,label,"Image Name"} %meta{string,widget,"filename"} %meta{int,texturefile,1} %read{55,94} %write{2147483647,-1} 6 | param int mapping 0 %meta{string,label,"Mapping"} %meta{string,widget,"mapper"} %meta{string,options,"Spherical (latlong):0|Angular:1"} %read{58,58} %write{2147483647,-1} 7 | param float intensity 1 %meta{string,label,"Intensity"} %meta{float,slidermin,0} %meta{float,slidermax,10} %read{0,0} %write{2147483647,-1} 8 | param float exposure 0 %meta{string,label,"Exposure"} %meta{float,slidermin,-5} %meta{float,slidermax,10} %read{1,1} %write{2147483647,-1} 9 | param color i_color 0.5 0.5 0.5 %meta{string,attribute,"color"} %meta{string,label,"Color"} %read{0,0} %write{2147483647,-1} 10 | param int contributions 1 %meta{string,page,"Contributions"} %meta{string,label,"Enable"} %meta{string,widget,"null"} %read{6,6} %write{2147483647,-1} 11 | param float diffuse_contribution 1 %meta{string,page,"Contributions"} %meta{string,label,"Diffuse"} %meta{float,slidermin,0} %meta{float,slidermax,3} %meta{float,min,0} %meta{string,lock_left,"contributions"} %meta{string,lock_op,"notEqualTo"} %meta{int,lock_right,1} %read{18,25} %write{2147483647,-1} 12 | param float specular_contribution 1 %meta{string,page,"Contributions"} %meta{string,label,"Specular"} %meta{float,slidermin,0} %meta{float,slidermax,3} %meta{float,min,0} %meta{string,lock_left,"contributions"} %meta{string,lock_op,"notEqualTo"} %meta{int,lock_right,1} %read{34,41} %write{2147483647,-1} 13 | param float hair_contribution 1 %meta{string,maya_attribute,"_3delight_hairContribution"} %meta{string,page,"Contributions"} %meta{string,label,"Hair"} %meta{float,slidermin,0} %meta{float,slidermax,3} %meta{float,min,0} %meta{string,lock_left,"contributions"} %meta{string,lock_op,"notEqualTo"} %meta{int,lock_right,1} %read{26,33} %write{2147483647,-1} 14 | param float volume_contribution 1 %meta{string,maya_attribute,"_3delight_volumeContribution"} %meta{string,page,"Contributions"} %meta{string,label,"Volume"} %meta{float,slidermin,0} %meta{float,slidermax,3} %meta{float,min,0} %meta{string,lock_left,"contributions"} %meta{string,lock_op,"notEqualTo"} %meta{int,lock_right,1} %read{42,49} %write{2147483647,-1} 15 | param float background_contribution 1 %meta{string,page,"Contributions"} %meta{string,label,"Background"} %meta{float,slidermin,0} %meta{float,slidermax,3} %meta{float,min,0} %meta{string,lock_left,"contributions"} %meta{string,lock_op,"notEqualTo"} %meta{int,lock_right,1} %read{10,17} %write{2147483647,-1} 16 | oparam closure color out %read{2147483647,-1} %write{53,100} 17 | global vector I %read{57,57} %write{2147483647,-1} %derivs 18 | global closure color Ci %read{100,100} %write{52,99} 19 | local color result %read{3,97} %write{2,95} 20 | local vector ___327_R %read{60,85} %write{57,57} %derivs 21 | local float ___327_s %read{94,94} %write{76,91} %derivs 22 | local float ___327_t %read{94,94} %write{77,93} %derivs 23 | local float ___328_signed_s %read{76,76} %write{64,64} %derivs 24 | local float ___328_signed_t %read{77,77} %write{75,75} %derivs 25 | local float ___329_signed_s %read{90,90} %write{83,83} %derivs 26 | local float ___329_signed_t %read{92,92} %write{89,89} %derivs 27 | temp color $tmp1 %read{2,2} %write{0,0} 28 | temp float $tmp2 %read{2,2} %write{1,1} 29 | const int $const1 2 %read{61,85} %write{2147483647,-1} 30 | const float $const2 2 %read{1,87} %write{2147483647,-1} 31 | const float $const3 0 %read{3,50} %write{2147483647,-1} 32 | temp int $tmp3 %read{4,4} %write{3,3} 33 | temp int $tmp4 %read{5,9} %write{4,8} 34 | const int $const4 0 %read{4,78} %write{2147483647,-1} 35 | const int $const5 1 %read{6,84} %write{2147483647,-1} 36 | temp int $tmp5 %read{7,7} %write{6,6} 37 | temp int $tmp6 %read{8,8} %write{7,7} 38 | const float $const6 1 %read{10,92} %write{2147483647,-1} 39 | temp int $tmp7 %read{11,11} %write{10,10} 40 | temp int $tmp8 %read{12,16} %write{11,15} 41 | temp int $tmp9 %read{14,14} %write{13,13} 42 | const string $const7 "camera" %read{13,13} %write{2147483647,-1} 43 | temp int $tmp10 %read{15,15} %write{14,14} 44 | temp int $tmp11 %read{19,19} %write{18,18} 45 | temp int $tmp12 %read{20,24} %write{19,23} 46 | temp int $tmp13 %read{22,22} %write{21,21} 47 | const string $const8 "diffuse" %read{21,21} %write{2147483647,-1} 48 | temp int $tmp14 %read{23,23} %write{22,22} 49 | temp int $tmp15 %read{27,27} %write{26,26} 50 | temp int $tmp16 %read{28,32} %write{27,31} 51 | temp int $tmp17 %read{30,30} %write{29,29} 52 | const string $const9 "hair" %read{29,29} %write{2147483647,-1} 53 | temp int $tmp18 %read{31,31} %write{30,30} 54 | temp int $tmp19 %read{35,35} %write{34,34} 55 | temp int $tmp20 %read{36,40} %write{35,39} 56 | temp int $tmp21 %read{38,38} %write{37,37} 57 | const string $const10 "specular" %read{37,37} %write{2147483647,-1} 58 | temp int $tmp22 %read{39,39} %write{38,38} 59 | temp int $tmp23 %read{43,43} %write{42,42} 60 | temp int $tmp24 %read{44,48} %write{43,47} 61 | temp int $tmp25 %read{46,46} %write{45,45} 62 | const string $const11 "volume" %read{45,45} %write{2147483647,-1} 63 | temp int $tmp26 %read{47,47} %write{46,46} 64 | temp int $tmp27 %read{51,51} %write{50,50} 65 | const string $const12 "" %read{55,55} %write{2147483647,-1} 66 | temp int $tmp28 %read{56,56} %write{55,55} 67 | temp int $tmp29 %read{59,59} %write{58,58} 68 | temp float $tmp30 %read{64,64} %write{62,62} %derivs 69 | temp float $tmp31 %read{62,62} %write{60,60} %derivs 70 | temp float $tmp32 %read{62,62} %write{61,61} %derivs 71 | const float $const13 3.14159274 %read{63,75} %write{2147483647,-1} 72 | temp float $tmp33 %read{64,64} %write{63,63} %derivs 73 | temp float $tmp34 %read{75,75} %write{74,74} %derivs 74 | temp float $tmp35 %read{74,74} %write{65,65} %derivs 75 | temp float $tmp36 %read{74,74} %write{73,73} %derivs 76 | temp float $tmp37 %read{68,68} %write{66,66} %derivs 77 | temp float $tmp38 %read{68,68} %write{67,67} %derivs 78 | temp float $tmp39 %read{72,72} %write{68,68} %derivs 79 | temp float $tmp40 %read{71,71} %write{69,69} %derivs 80 | temp float $tmp41 %read{71,71} %write{70,70} %derivs 81 | temp float $tmp42 %read{72,72} %write{71,71} %derivs 82 | temp float $tmp43 %read{73,73} %write{72,72} %derivs 83 | const float $const14 0.5 %read{76,93} %write{2147483647,-1} 84 | temp float $tmp44 %read{83,83} %write{78,78} %derivs 85 | temp float $tmp45 %read{83,83} %write{82,82} %derivs 86 | temp float $tmp46 %read{80,80} %write{79,79} %derivs 87 | temp float $tmp47 %read{81,81} %write{80,80} %derivs 88 | temp float $tmp48 %read{82,82} %write{81,81} %derivs 89 | temp float $tmp49 %read{89,89} %write{84,84} %derivs 90 | temp float $tmp50 %read{89,89} %write{88,88} %derivs 91 | temp float $tmp51 %read{86,86} %write{85,85} %derivs 92 | temp float $tmp52 %read{87,87} %write{86,86} %derivs 93 | temp float $tmp53 %read{88,88} %write{87,87} %derivs 94 | temp float $tmp54 %read{91,91} %write{90,90} %derivs 95 | temp float $tmp55 %read{93,93} %write{92,92} %derivs 96 | temp color $tmp56 %read{95,95} %write{94,94} 97 | temp closure color $tmp57 %read{97,97} %write{96,96} 98 | const string $const15 "emission" %read{96,96} %write{2147483647,-1} 99 | temp closure color $tmp58 %read{99,99} %write{97,97} 100 | temp closure color $tmp59 %read{99,99} %write{98,98} 101 | const string $const16 "transparent" %read{98,98} %write{2147483647,-1} 102 | code ___main___ 103 | mul $tmp1 i_color intensity %filename{"environmentLight.osl"} %line{102} %argrw{"wrr"} 104 | pow $tmp2 $const2 exposure %argrw{"wrr"} 105 | mul result $tmp1 $tmp2 %argrw{"wrr"} 106 | neq $tmp3 result $const3 %line{108} %argrw{"wrr"} 107 | neq $tmp4 $tmp3 $const4 %argrw{"wrr"} 108 | if $tmp4 9 9 %argrw{"r"} 109 | eq $tmp5 contributions $const5 %argrw{"wrr"} 110 | neq $tmp6 $tmp5 $const4 %argrw{"wrr"} 111 | assign $tmp4 $tmp6 %argrw{"wr"} 112 | if $tmp4 50 50 %argrw{"r"} 113 | neq $tmp7 background_contribution $const6 %line{110} %argrw{"wrr"} 114 | neq $tmp8 $tmp7 $const4 %argrw{"wrr"} 115 | if $tmp8 16 16 %argrw{"r"} 116 | raytype $tmp9 $const7 %argrw{"wr"} 117 | neq $tmp10 $tmp9 $const4 %argrw{"wrr"} 118 | assign $tmp8 $tmp10 %argrw{"wr"} 119 | if $tmp8 18 50 %argrw{"r"} 120 | mul result result background_contribution %line{111} %argrw{"wrr"} 121 | neq $tmp11 diffuse_contribution $const6 %line{112} %argrw{"wrr"} 122 | neq $tmp12 $tmp11 $const4 %argrw{"wrr"} 123 | if $tmp12 24 24 %argrw{"r"} 124 | raytype $tmp13 $const8 %argrw{"wr"} 125 | neq $tmp14 $tmp13 $const4 %argrw{"wrr"} 126 | assign $tmp12 $tmp14 %argrw{"wr"} 127 | if $tmp12 26 50 %argrw{"r"} 128 | mul result result diffuse_contribution %line{113} %argrw{"wrr"} 129 | neq $tmp15 hair_contribution $const6 %line{114} %argrw{"wrr"} 130 | neq $tmp16 $tmp15 $const4 %argrw{"wrr"} 131 | if $tmp16 32 32 %argrw{"r"} 132 | raytype $tmp17 $const9 %argrw{"wr"} 133 | neq $tmp18 $tmp17 $const4 %argrw{"wrr"} 134 | assign $tmp16 $tmp18 %argrw{"wr"} 135 | if $tmp16 34 50 %argrw{"r"} 136 | mul result result hair_contribution %line{115} %argrw{"wrr"} 137 | neq $tmp19 specular_contribution $const6 %line{116} %argrw{"wrr"} 138 | neq $tmp20 $tmp19 $const4 %argrw{"wrr"} 139 | if $tmp20 40 40 %argrw{"r"} 140 | raytype $tmp21 $const10 %argrw{"wr"} 141 | neq $tmp22 $tmp21 $const4 %argrw{"wrr"} 142 | assign $tmp20 $tmp22 %argrw{"wr"} 143 | if $tmp20 42 50 %argrw{"r"} 144 | mul result result specular_contribution %line{117} %argrw{"wrr"} 145 | neq $tmp23 volume_contribution $const6 %line{118} %argrw{"wrr"} 146 | neq $tmp24 $tmp23 $const4 %argrw{"wrr"} 147 | if $tmp24 48 48 %argrw{"r"} 148 | raytype $tmp25 $const11 %argrw{"wr"} 149 | neq $tmp26 $tmp25 $const4 %argrw{"wrr"} 150 | assign $tmp24 $tmp26 %argrw{"wr"} 151 | if $tmp24 50 50 %argrw{"r"} 152 | mul result result volume_contribution %line{119} %argrw{"wrr"} 153 | eq $tmp27 result $const3 %line{122} %argrw{"wrr"} 154 | if $tmp27 55 55 %argrw{"r"} 155 | assign Ci $const4 %line{124} %argrw{"wr"} 156 | assign out $const4 %argrw{"wr"} 157 | exit %line{125} 158 | neq $tmp28 image $const12 %line{131} %argrw{"wrr"} 159 | if $tmp28 96 96 %argrw{"r"} 160 | normalize ___327_R I %line{133} %argrw{"wr"} 161 | eq $tmp29 mapping $const4 %line{136} %argrw{"wrr"} 162 | if $tmp29 78 94 %argrw{"r"} 163 | compref $tmp31 ___327_R $const4 %line{139} %argrw{"wrr"} 164 | compref $tmp32 ___327_R $const1 %argrw{"wrr"} 165 | atan2 $tmp30 $tmp31 $tmp32 %argrw{"wrr"} 166 | mul $tmp33 $const2 $const13 %argrw{"wrr"} 167 | div ___328_signed_s $tmp30 $tmp33 %argrw{"wrr"} 168 | compref $tmp35 ___327_R $const5 %line{140} %argrw{"wrr"} 169 | compref $tmp37 ___327_R $const4 %argrw{"wrr"} 170 | compref $tmp38 ___327_R $const4 %argrw{"wrr"} 171 | mul $tmp39 $tmp37 $tmp38 %argrw{"wrr"} 172 | compref $tmp40 ___327_R $const1 %argrw{"wrr"} 173 | compref $tmp41 ___327_R $const1 %argrw{"wrr"} 174 | mul $tmp42 $tmp40 $tmp41 %argrw{"wrr"} 175 | add $tmp43 $tmp39 $tmp42 %argrw{"wrr"} 176 | sqrt $tmp36 $tmp43 %argrw{"wr"} 177 | atan2 $tmp34 $tmp35 $tmp36 %argrw{"wrr"} 178 | div ___328_signed_t $tmp34 $const13 %argrw{"wrr"} 179 | add ___327_s ___328_signed_s $const14 %line{142} %argrw{"wrr"} 180 | add ___327_t ___328_signed_t $const14 %line{143} %argrw{"wrr"} 181 | compref $tmp44 ___327_R $const4 %line{148} %argrw{"wrr"} 182 | compref $tmp46 ___327_R $const1 %argrw{"wrr"} 183 | add $tmp47 $tmp46 $const6 %argrw{"wrr"} 184 | mul $tmp48 $const2 $tmp47 %argrw{"wrr"} 185 | sqrt $tmp45 $tmp48 %argrw{"wr"} 186 | div ___329_signed_s $tmp44 $tmp45 %argrw{"wrr"} 187 | compref $tmp49 ___327_R $const5 %line{149} %argrw{"wrr"} 188 | compref $tmp51 ___327_R $const1 %argrw{"wrr"} 189 | add $tmp52 $tmp51 $const6 %argrw{"wrr"} 190 | mul $tmp53 $const2 $tmp52 %argrw{"wrr"} 191 | sqrt $tmp50 $tmp53 %argrw{"wr"} 192 | div ___329_signed_t $tmp49 $tmp50 %argrw{"wrr"} 193 | add $tmp54 ___329_signed_s $const6 %line{151} %argrw{"wrr"} 194 | mul ___327_s $tmp54 $const14 %argrw{"wrr"} 195 | add $tmp55 ___329_signed_t $const6 %line{152} %argrw{"wrr"} 196 | mul ___327_t $tmp55 $const14 %argrw{"wrr"} 197 | texture $tmp56 image ___327_s ___327_t %line{155} %argrw{"wrrr"} %argderivs{2,3} 198 | mul result result $tmp56 %argrw{"wrr"} 199 | closure $tmp57 $const15 %line{158} %argrw{"wr"} 200 | mul $tmp58 $tmp57 result %argrw{"wrr"} 201 | closure $tmp59 $const16 %argrw{"wr"} 202 | add Ci $tmp58 $tmp59 %argrw{"wrr"} 203 | assign out Ci %argrw{"wr"} 204 | end 205 | -------------------------------------------------------------------------------- /polyhedron.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualritz/polyhedron-ops/537ea0f0973be1cfb77316008b7f0002aeac2016/polyhedron.jpg -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | unstable_features = true 3 | imports_granularity = "Crate" 4 | use_field_init_shorthand = true 5 | reorder_impl_items = true 6 | wrap_comments = true 7 | format_code_in_doc_comments = true 8 | newline_style = "Unix" 9 | -------------------------------------------------------------------------------- /src/base_polyhedra.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use num_traits::float::FloatConst; 3 | 4 | /// # Base Shapes 5 | /// 6 | /// Start shape creation methods. 7 | impl Polyhedron { 8 | pub fn tetrahedron() -> Self { 9 | let c0 = 1.0; 10 | 11 | Self { 12 | positions: vec![ 13 | Point::new(c0, c0, c0), 14 | Point::new(c0, -c0, -c0), 15 | Point::new(-c0, c0, -c0), 16 | Point::new(-c0, -c0, c0), 17 | ], 18 | face_index: vec![ 19 | vec![2, 1, 0], 20 | vec![3, 2, 0], 21 | vec![1, 3, 0], 22 | vec![2, 3, 1], 23 | ], 24 | face_set_index: vec![(0..4).collect()], 25 | name: String::from("T"), 26 | } 27 | } 28 | 29 | pub fn hexahedron() -> Self { 30 | let c0 = 1.0; 31 | 32 | Self { 33 | positions: vec![ 34 | Point::new(c0, c0, c0), 35 | Point::new(c0, c0, -c0), 36 | Point::new(c0, -c0, c0), 37 | Point::new(c0, -c0, -c0), 38 | Point::new(-c0, c0, c0), 39 | Point::new(-c0, c0, -c0), 40 | Point::new(-c0, -c0, c0), 41 | Point::new(-c0, -c0, -c0), 42 | ], 43 | face_index: vec![ 44 | vec![4, 5, 1, 0], 45 | vec![2, 6, 4, 0], 46 | vec![1, 3, 2, 0], 47 | vec![6, 2, 3, 7], 48 | vec![5, 4, 6, 7], 49 | vec![3, 1, 5, 7], 50 | ], 51 | face_set_index: vec![(0..6).collect()], 52 | name: String::from("C"), 53 | } 54 | } 55 | 56 | #[inline] 57 | /// Alias for [`hexahedron()`](Self::hexahedron()). 58 | pub fn cube() -> Self { 59 | Self::hexahedron() 60 | } 61 | 62 | pub fn octahedron() -> Self { 63 | let c0 = 0.707_106_77; 64 | 65 | Self { 66 | positions: vec![ 67 | Point::new(0.0, 0.0, c0), 68 | Point::new(0.0, 0.0, -c0), 69 | Point::new(c0, 0.0, 0.0), 70 | Point::new(-c0, 0.0, 0.0), 71 | Point::new(0.0, c0, 0.0), 72 | Point::new(0.0, -c0, 0.0), 73 | ], 74 | face_index: vec![ 75 | vec![4, 2, 0], 76 | vec![3, 4, 0], 77 | vec![5, 3, 0], 78 | vec![2, 5, 0], 79 | vec![5, 2, 1], 80 | vec![3, 5, 1], 81 | vec![4, 3, 1], 82 | vec![2, 4, 1], 83 | ], 84 | face_set_index: vec![(0..8).collect()], 85 | name: String::from("O"), 86 | } 87 | } 88 | 89 | pub fn dodecahedron() -> Self { 90 | let c0 = 0.809_017; 91 | let c1 = 1.309_017; 92 | 93 | Self { 94 | positions: vec![ 95 | Point::new(0.0, 0.5, c1), 96 | Point::new(0.0, 0.5, -c1), 97 | Point::new(0.0, -0.5, c1), 98 | Point::new(0.0, -0.5, -c1), 99 | Point::new(c1, 0.0, 0.5), 100 | Point::new(c1, 0.0, -0.5), 101 | Point::new(-c1, 0.0, 0.5), 102 | Point::new(-c1, 0.0, -0.5), 103 | Point::new(0.5, c1, 0.0), 104 | Point::new(0.5, -c1, 0.0), 105 | Point::new(-0.5, c1, 0.0), 106 | Point::new(-0.5, -c1, 0.0), 107 | Point::new(c0, c0, c0), 108 | Point::new(c0, c0, -c0), 109 | Point::new(c0, -c0, c0), 110 | Point::new(c0, -c0, -c0), 111 | Point::new(-c0, c0, c0), 112 | Point::new(-c0, c0, -c0), 113 | Point::new(-c0, -c0, c0), 114 | Point::new(-c0, -c0, -c0), 115 | ], 116 | face_index: vec![ 117 | vec![12, 4, 14, 2, 0], 118 | vec![16, 10, 8, 12, 0], 119 | vec![2, 18, 6, 16, 0], 120 | vec![17, 10, 16, 6, 7], 121 | vec![19, 3, 1, 17, 7], 122 | vec![6, 18, 11, 19, 7], 123 | vec![15, 3, 19, 11, 9], 124 | vec![14, 4, 5, 15, 9], 125 | vec![11, 18, 2, 14, 9], 126 | vec![8, 10, 17, 1, 13], 127 | vec![5, 4, 12, 8, 13], 128 | vec![1, 3, 15, 5, 13], 129 | ], 130 | face_set_index: vec![(0..12).collect()], 131 | name: String::from("D"), 132 | } 133 | } 134 | 135 | pub fn icosahedron() -> Self { 136 | let c0 = 0.809_017; 137 | 138 | Self { 139 | positions: vec![ 140 | Point::new(0.5, 0.0, c0), 141 | Point::new(0.5, 0.0, -c0), 142 | Point::new(-0.5, 0.0, c0), 143 | Point::new(-0.5, 0.0, -c0), 144 | Point::new(c0, 0.5, 0.0), 145 | Point::new(c0, -0.5, 0.0), 146 | Point::new(-c0, 0.5, 0.0), 147 | Point::new(-c0, -0.5, 0.0), 148 | Point::new(0.0, c0, 0.5), 149 | Point::new(0.0, c0, -0.5), 150 | Point::new(0.0, -c0, 0.5), 151 | Point::new(0.0, -c0, -0.5), 152 | ], 153 | face_index: vec![ 154 | vec![10, 2, 0], 155 | vec![5, 10, 0], 156 | vec![4, 5, 0], 157 | vec![8, 4, 0], 158 | vec![2, 8, 0], 159 | vec![6, 8, 2], 160 | vec![7, 6, 2], 161 | vec![10, 7, 2], 162 | vec![11, 7, 10], 163 | vec![5, 11, 10], 164 | vec![1, 11, 5], 165 | vec![4, 1, 5], 166 | vec![9, 1, 4], 167 | vec![8, 9, 4], 168 | vec![6, 9, 8], 169 | vec![3, 9, 6], 170 | vec![7, 3, 6], 171 | vec![11, 3, 7], 172 | vec![1, 3, 11], 173 | vec![9, 3, 1], 174 | ], 175 | face_set_index: vec![(0..20).collect()], 176 | name: String::from("I"), 177 | } 178 | } 179 | 180 | /// common code for prism and antiprism 181 | #[inline] 182 | fn protoprism(n: Option, anti: bool) -> Self { 183 | let n = n.unwrap_or(3); 184 | 185 | // Angles. 186 | let theta = f32::TAU() / n as f32; 187 | let twist = if anti { theta / 2.0 } else { 0.0 }; 188 | // Half-edge. 189 | let h = (theta * 0.5).sin(); 190 | 191 | let mut face_index = vec![ 192 | (0..n).map(|i| i as VertexKey).collect::>(), 193 | (n..2 * n).rev().map(|i| i as VertexKey).collect::>(), 194 | ]; 195 | 196 | // Sides. 197 | if anti { 198 | face_index.extend( 199 | (0..n) 200 | .map(|i| { 201 | vec![ 202 | i as VertexKey, 203 | (i + n) as VertexKey, 204 | ((i + 1) % n) as VertexKey, 205 | ] 206 | }) 207 | .chain((0..n).map(|i| { 208 | vec![ 209 | (i + n) as VertexKey, 210 | ((i + 1) % n + n) as VertexKey, 211 | ((i + 1) % n) as VertexKey, 212 | ] 213 | })), 214 | ); 215 | } else { 216 | face_index.extend((0..n).map(|i| { 217 | vec![ 218 | i as VertexKey, 219 | (i + n) as VertexKey, 220 | ((i + 1) % n + n) as VertexKey, 221 | ((i + 1) % n) as VertexKey, 222 | ] 223 | })); 224 | }; 225 | 226 | Self { 227 | name: format!("{}{}", if anti { "A" } else { "P" }, n), 228 | positions: (0..n) 229 | .map(move |i| { 230 | Point::new( 231 | (i as f32 * theta).cos() as _, 232 | h, 233 | (i as f32 * theta).sin() as _, 234 | ) 235 | }) 236 | .chain((0..n).map(move |i| { 237 | Point::new( 238 | (twist + i as f32 * theta).cos() as _, 239 | -h, 240 | (twist + i as f32 * theta).sin() as _, 241 | ) 242 | })) 243 | .collect(), 244 | 245 | face_index, 246 | face_set_index: Vec::new(), 247 | } 248 | } 249 | 250 | pub fn prism(n: Option) -> Self { 251 | Self::protoprism(n, false) 252 | } 253 | 254 | pub fn antiprism(n: Option) -> Self { 255 | Self::protoprism(n, true) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/grammar.pest: -------------------------------------------------------------------------------- 1 | WHITESPACE = _{ " " | "\t" | "\n" | "\r" } 2 | float = @{ int ~ ("." ~ ASCII_DIGIT*)? ~ (^"e" ~ int)? } 3 | int = { ("+" | "-")? ~ ASCII_DIGIT+ } 4 | ufloat = @{ uint ~ ("." ~ ASCII_DIGIT*)? ~ (^"e" ~ int)? } 5 | uint = { ASCII_DIGIT+ } 6 | uint_array = _{ 7 | "[" 8 | ~ 9 | uint 10 | ~ ( 11 | "," 12 | ~ 13 | uint 14 | )* 15 | ~ 16 | "]" 17 | } 18 | bool = { "{t}" | "{f}" } 19 | tetrahedron = { "T" } 20 | hexahedron = { "C" } 21 | octahedron = { "O" } 22 | dodecahedron = { "D" } 23 | icosahedron = { "I" } 24 | prism = { "P" ~ uint } 25 | antiprism = { "A" ~ uint } 26 | //pyramid = { "Y" ~ uint } 27 | //johnson_solid = { "J" ~ uint } 28 | base_shape = _{ tetrahedron | hexahedron | octahedron | dodecahedron | icosahedron | prism | antiprism } 29 | separator = { "," } 30 | ambo = { "a" ~ (ufloat)? } 31 | // bevel uf_ratio, f_height, ui_vertex_degree, b_regular_faces_only 32 | bevel = { "b" ~ 33 | ( 34 | (ufloat)? 35 | ~ 36 | ( 37 | separator 38 | ~ 39 | (float)? 40 | ~ 41 | ( 42 | separator 43 | ~ 44 | (uint | uint_array)? 45 | ~ 46 | ( 47 | separator 48 | ~ 49 | (bool)? 50 | )? 51 | )? 52 | )? 53 | )? 54 | } 55 | // Catmull-Clark subdivide 56 | catmull_clark_subdivide = { "v" } 57 | // chamfer uf_ratio 58 | chamfer = { "c" ~ (ufloat)? } 59 | dual = { "d" } 60 | expand = { "e" ~ (ufloat)? } 61 | // extrude f_height, f_offset, ui_face_arity_mask 62 | extrude = { "x" ~ 63 | ( 64 | (float)? 65 | ~ 66 | ( 67 | separator 68 | ~ 69 | (float)? 70 | ~ 71 | ( 72 | separator 73 | ~ 74 | (uint | uint_array)? 75 | )? 76 | )? 77 | )? 78 | } 79 | // gyro uf_ratio, f_height 80 | gyro = { "g" ~ ( (ufloat)? ~ (separator ~ (float)? )? )? } 81 | // inset f_offset 82 | inset = { "i" ~ (ufloat)? } 83 | // join uf_ratio 84 | join = { "j" ~ (ufloat)? } 85 | // kis f_height, ui_face_arity_mask, ui_face_index_mask, b_regular_faces_only 86 | kis = { "k" ~ 87 | ( 88 | (float)? 89 | ~ 90 | ( 91 | separator 92 | ~ 93 | (uint | uint_array)? 94 | ~ 95 | ( 96 | separator 97 | ~ 98 | (uint | uint_array)? 99 | ~ 100 | ( 101 | separator 102 | ~ 103 | (bool)? 104 | )? 105 | )? 106 | )? 107 | )? 108 | } 109 | // medial uf_ratio, f_height, ui_vertex_valence, b_regular_faces_only 110 | medial = { "M" ~ 111 | ( 112 | (ufloat)? 113 | ~ 114 | ( 115 | separator 116 | ~ 117 | (float)? 118 | ~ 119 | ( 120 | separator 121 | ~ 122 | (uint | uint_array)? 123 | ~ 124 | ( 125 | separator 126 | ~ 127 | (bool)? 128 | )? 129 | )? 130 | )? 131 | )? 132 | } 133 | // meta uf_ratio, f_height, ui_vertex_valence, b_regular_faces_only 134 | meta = { "m" ~ 135 | ( 136 | (ufloat)? 137 | ~ 138 | ( 139 | separator 140 | ~ 141 | (float)? 142 | ~ 143 | ( 144 | separator 145 | ~ 146 | (uint | uint_array)? 147 | ~ 148 | ( 149 | separator 150 | ~ 151 | (bool)? 152 | )? 153 | )? 154 | )? 155 | )? 156 | } 157 | // needle f_height, ui_vertex_valence, b_regular_faces_only 158 | needle = { "n" ~ 159 | ( 160 | (float)? 161 | ~ 162 | ( 163 | separator 164 | ~ 165 | (uint | uint_array)? 166 | ~ 167 | ( 168 | separator 169 | ~ 170 | (bool)? 171 | )? 172 | )? 173 | )? 174 | } 175 | // ortho uf_ratio 176 | ortho = { "o" ~ (ufloat)? } 177 | // planarize ui_iterations 178 | planarize = { "K" ~ (uint)? } 179 | // propellor uf_ratio 180 | propellor = { "p" ~ (ufloat)? } 181 | // quinto f_height 182 | quinto = { "q" ~ (float)? } 183 | // reflect 184 | reflect = { "r" } 185 | // snub uf_ratio, f_height 186 | snub = { "s" ~ ( (ufloat)? ~ (separator ~ (float)? )? )? } 187 | // spherize uf_strength 188 | spherize = { "S" ~ (ufloat)? } 189 | // truncate f_height, ui_vertex_valence, b_regular_faces_only 190 | truncate = { "t" ~ 191 | ( 192 | (float)? 193 | ~ 194 | ( 195 | separator 196 | ~ 197 | (uint | uint_array)? 198 | ~ 199 | ( 200 | separator 201 | ~ 202 | (bool)? 203 | )? 204 | )? 205 | )? 206 | } 207 | // whirl uf_ratio, f_height 208 | whirl = { "w" ~ ( (ufloat)? ~ (separator ~ (float)? )? )? } 209 | // zip f_height, ui_vertex_valence, b_regular_faces_only 210 | zip = { "z" ~ 211 | ( 212 | (float)? 213 | ~ 214 | ( 215 | separator 216 | ~ 217 | (uint | uint_array)? 218 | ~ 219 | ( 220 | separator 221 | ~ 222 | (bool)? 223 | )? 224 | )? 225 | )? 226 | } 227 | operation = _{ 228 | ambo 229 | | bevel 230 | | catmull_clark_subdivide 231 | | chamfer 232 | | dual 233 | | expand 234 | | extrude 235 | | gyro 236 | | inset 237 | | join 238 | | kis 239 | | medial 240 | | meta 241 | | needle 242 | | ortho 243 | | planarize 244 | | propellor 245 | | quinto 246 | | reflect 247 | | snub 248 | | spherize 249 | | truncate 250 | | whirl 251 | | zip 252 | } 253 | operation_chain = _{ operation ~ (operation)* } 254 | conway_notation_string = _{SOI ~ (operation_chain)? ~ base_shape ~ EOI} 255 | 256 | 257 | //command = { operation ~ (num ~("," ~num)*)* } -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use uv::DVec3; 3 | 4 | // Extend a vector with some element(s) 5 | // ``` 6 | // extend![..foo, 4, 5, 6] 7 | // ``` 8 | #[doc(hidden)] 9 | #[macro_export] 10 | macro_rules! extend { 11 | (..$v:expr, $($new:expr),*) => {{ 12 | let mut tmp = $v.clone(); 13 | $( 14 | tmp.push($new); 15 | )* 16 | tmp 17 | }} 18 | } 19 | 20 | #[inline] 21 | pub(crate) fn _to_vadd(positions: &PointsSlice, v: &Vector) -> Points { 22 | positions.par_iter().map(|p| *p + *v).collect() 23 | } 24 | 25 | #[inline] 26 | pub(crate) fn vadd(positions: &mut Points, v: &Vector) { 27 | positions.par_iter_mut().for_each(|p| *p += *v); 28 | } 29 | 30 | #[inline] 31 | pub(crate) fn centroid(positions: &PointsSlice) -> Point { 32 | positions 33 | .iter() 34 | .fold(Point::zero(), |accumulate, point| accumulate + *point) 35 | //.into_par_iter() 36 | //.cloned() 37 | //.reduce(|| Point::zero(), |accumulate, point| accumulate + point); 38 | / positions.len() as Float 39 | } 40 | 41 | #[inline] 42 | pub(crate) fn centroid_ref(positions: &PointsRefSlice) -> Point { 43 | positions 44 | .iter() 45 | .fold(Point::zero(), |accumulate, point| accumulate + **point) 46 | / positions.len() as Float 47 | } 48 | 49 | // Centroid projected onto the spherical surface that passes to the average of 50 | // the given positions with the center at the origin. 51 | #[inline] 52 | pub(crate) fn _centroid_spherical_ref( 53 | positions: &PointsRefSlice, 54 | spherical: Float, 55 | ) -> Point { 56 | let point: Point = positions 57 | .iter() 58 | .fold(Point::zero(), |sum, point| sum + **point) 59 | / positions.len() as Float; 60 | 61 | if spherical != 0.0 { 62 | let avg_mag = 63 | positions.iter().fold(0.0, |sum, point| sum + point.mag()) 64 | / positions.len() as Float; 65 | 66 | point * ((1.0 - spherical) + spherical * (point.mag() / avg_mag)) 67 | } else { 68 | point 69 | } 70 | } 71 | 72 | /// Return the ordered edges containing v 73 | pub(crate) fn vertex_edges(v: VertexKey, edges: &EdgesSlice) -> Edges { 74 | edges 75 | .iter() 76 | .filter_map(|edge| { 77 | if edge[0] == v || edge[1] == v { 78 | Some(*edge) 79 | } else { 80 | None 81 | } 82 | }) 83 | .collect() 84 | } 85 | 86 | #[inline] 87 | pub(crate) fn ordered_vertex_edges_recurse( 88 | v: VertexKey, 89 | vfaces: &FacesSlice, 90 | face: &FaceSlice, 91 | k: usize, 92 | ) -> Edges { 93 | if k < vfaces.len() { 94 | let i = index_of(&v, face).unwrap(); 95 | /*match index_of(&v, face) { 96 | Some(i) => i, 97 | None => return vec![], 98 | };*/ 99 | let j = (i + face.len() - 1) % face.len(); 100 | let edge = [v, face[j]]; 101 | let nface = face_with_edge(&edge, vfaces); 102 | let mut result = vec![edge]; 103 | result.extend(ordered_vertex_edges_recurse(v, vfaces, &nface, k + 1)); 104 | result 105 | } else { 106 | vec![] 107 | } 108 | } 109 | 110 | #[inline] 111 | pub(crate) fn ordered_vertex_edges(v: VertexKey, vfaces: &FacesSlice) -> Edges { 112 | if vfaces.is_empty() { 113 | vec![] 114 | } else { 115 | let face = &vfaces[0]; 116 | let i = index_of(&v, face).unwrap(); 117 | let j = (i + face.len() - 1) % face.len(); 118 | let edge = [v, face[j]]; 119 | let nface = face_with_edge(&edge, vfaces); 120 | let mut result = vec![edge]; 121 | result.extend(ordered_vertex_edges_recurse(v, vfaces, &nface, 1)); 122 | result 123 | } 124 | } 125 | 126 | #[inline] 127 | pub(crate) fn positions_to_faces( 128 | positions: &PointsSlice, 129 | face_index: &FacesSlice, 130 | ) -> Faces { 131 | positions 132 | .par_iter() 133 | .enumerate() 134 | .map(|vertex| { 135 | // Each old vertex creates a new face. 136 | ordered_vertex_faces( 137 | vertex.0 as VertexKey, 138 | &vertex_faces(vertex.0 as VertexKey, face_index), 139 | ) 140 | .iter() 141 | .map(|original_face| 142 | // With vertex faces in left-hand order. 143 | index_of(original_face, face_index).unwrap() as VertexKey) 144 | .collect() 145 | }) 146 | .collect() 147 | } 148 | 149 | #[inline] 150 | pub(crate) fn distinct_edge(edge: &Edge) -> Edge { 151 | if edge[0] < edge[1] { 152 | *edge 153 | } else { 154 | [edge[1], edge[0]] 155 | } 156 | } 157 | 158 | #[inline] 159 | pub(crate) fn distinct_face_edges(face: &FaceSlice) -> Edges { 160 | face.iter() 161 | .circular_tuple_windows::<(_, _)>() 162 | .map(|t| { 163 | if t.0 < t.1 { 164 | [*t.0, *t.1] 165 | } else { 166 | [*t.1, *t.0] 167 | } 168 | }) 169 | .collect() 170 | } 171 | 172 | #[inline] 173 | pub(crate) fn _to_centroid_positions(positions: &PointsSlice) -> Points { 174 | _to_vadd(positions, &-centroid(positions)) 175 | } 176 | 177 | #[inline] 178 | pub(crate) fn center_on_centroid(positions: &mut Points) { 179 | vadd(positions, &-centroid(positions)); 180 | } 181 | 182 | #[inline] 183 | pub(crate) fn vnorm(positions: &PointsSlice) -> Vec { 184 | positions.par_iter().map(|v| v.mag()).collect() 185 | } 186 | // Was: average_norm 187 | #[inline] 188 | pub(crate) fn _average_magnitude(positions: &PointsSlice) -> Float { 189 | vnorm(positions).par_iter().sum::() / positions.len() as Float 190 | } 191 | 192 | #[inline] 193 | pub(crate) fn max_magnitude(positions: &PointsSlice) -> Float { 194 | vnorm(positions) 195 | .into_par_iter() 196 | .reduce(|| Float::NAN, Float::max) 197 | } 198 | 199 | /// Returns a [`Faces`] of faces 200 | /// containing `vertex_number`. 201 | #[inline] 202 | pub(crate) fn vertex_faces( 203 | vertex_number: VertexKey, 204 | face_index: &FacesSlice, 205 | ) -> Faces { 206 | face_index 207 | .par_iter() 208 | .filter(|face| face.contains(&vertex_number)) 209 | .cloned() 210 | .collect() 211 | } 212 | 213 | /// Returns a [`Vec`] of anticlockwise 214 | /// ordered edges. 215 | pub(crate) fn _ordered_face_edges_(face: &FaceSlice) -> Edges { 216 | face.iter() 217 | .circular_tuple_windows::<(_, _)>() 218 | .map(|edge| [*edge.0, *edge.1]) 219 | .collect() 220 | } 221 | 222 | /// Returns a [`Vec`] of anticlockwise 223 | /// ordered edges. 224 | #[inline] 225 | pub(crate) fn ordered_face_edges(face: &FaceSlice) -> Edges { 226 | (0..face.len()) 227 | .map(|i| [face[i], face[(i + 1) % face.len()]]) 228 | .collect() 229 | } 230 | 231 | #[inline] 232 | pub(crate) fn face_with_edge(edge: &Edge, faces: &FacesSlice) -> Face { 233 | let result = faces 234 | .par_iter() 235 | .filter(|face| ordered_face_edges(face).contains(edge)) 236 | .flatten() 237 | .cloned() 238 | .collect(); 239 | result 240 | } 241 | 242 | #[inline] 243 | pub(crate) fn index_of(element: &T, list: &[T]) -> Option { 244 | list.iter().position(|e| *e == *element) 245 | } 246 | 247 | /// Used internally by [`ordered_vertex_faces()`]. 248 | #[inline] 249 | pub(crate) fn ordered_vertex_faces_recurse( 250 | v: VertexKey, 251 | face_index: &FacesSlice, 252 | cface: &FaceSlice, 253 | k: VertexKey, 254 | ) -> Faces { 255 | if (k as usize) < face_index.len() { 256 | let i = index_of(&v, cface).unwrap() as i32; 257 | let j = ((i - 1 + cface.len() as i32) % cface.len() as i32) as usize; 258 | let edge = [v, cface[j]]; 259 | let mut nfaces = vec![face_with_edge(&edge, face_index)]; 260 | nfaces.extend(ordered_vertex_faces_recurse( 261 | v, 262 | face_index, 263 | &nfaces[0], 264 | k + 1, 265 | )); 266 | nfaces 267 | } else { 268 | Faces::new() 269 | } 270 | } 271 | 272 | #[inline] 273 | pub(crate) fn ordered_vertex_faces( 274 | vertex_number: VertexKey, 275 | face_index: &FacesSlice, 276 | ) -> Faces { 277 | let mut result = vec![face_index[0].clone()]; 278 | result.extend(ordered_vertex_faces_recurse( 279 | vertex_number, 280 | face_index, 281 | &face_index[0], 282 | 1, 283 | )); 284 | 285 | result 286 | } 287 | 288 | #[inline] 289 | pub(crate) fn edge_length(edge: &Edge, positions: &PointsSlice) -> Float { 290 | let edge = vec![edge[0], edge[1]]; 291 | let positions = index_as_positions(&edge, positions); 292 | (*positions[0] - *positions[1]).mag() 293 | } 294 | 295 | #[inline] 296 | pub(crate) fn _edge_lengths( 297 | edges: &_EdgeSlice, 298 | positions: &PointsSlice, 299 | ) -> Vec { 300 | edges 301 | .par_iter() 302 | .map(|edge| edge_length(edge, positions)) 303 | .collect() 304 | } 305 | 306 | #[inline] 307 | pub(crate) fn face_edges( 308 | face: &FaceSlice, 309 | positions: &PointsSlice, 310 | ) -> Vec { 311 | ordered_face_edges(face) 312 | .par_iter() 313 | .map(|edge| edge_length(edge, positions)) 314 | .collect() 315 | } 316 | 317 | #[inline] 318 | pub(crate) fn _circumscribed_resize(positions: &mut Points, radius: Float) { 319 | center_on_centroid(positions); 320 | let average = _average_magnitude(positions); 321 | 322 | positions 323 | .par_iter_mut() 324 | .for_each(|v| *v *= radius / average); 325 | } 326 | 327 | pub(crate) fn max_resize(positions: &mut Points, radius: Float) { 328 | center_on_centroid(positions); 329 | let max = max_magnitude(positions); 330 | 331 | positions.par_iter_mut().for_each(|v| *v *= radius / max); 332 | } 333 | 334 | #[inline] 335 | pub(crate) fn _project_on_sphere(positions: &mut Points, radius: Float) { 336 | positions 337 | .par_iter_mut() 338 | .for_each(|point| *point = radius * point.normalized()); 339 | } 340 | 341 | #[inline] 342 | pub(crate) fn face_irregularity( 343 | face: &FaceSlice, 344 | positions: &PointsSlice, 345 | ) -> Float { 346 | let lengths = face_edges(face, positions); 347 | // The largest value in lengths or NaN (0./0.) otherwise. 348 | lengths.par_iter().cloned().reduce(|| Float::NAN, Float::max) 349 | // divide by the smallest value in lengths or NaN (0./0.) otherwise. 350 | / lengths.par_iter().cloned().reduce(|| Float::NAN, Float::min) 351 | } 352 | 353 | #[inline] 354 | pub(crate) fn index_as_positions<'a>( 355 | f: &[VertexKey], 356 | positions: &'a PointsSlice, 357 | ) -> Vec<&'a Point> { 358 | f.par_iter() 359 | .map(|index| &positions[*index as usize]) 360 | .collect() 361 | } 362 | 363 | #[inline] 364 | pub(crate) fn _planar_area_ref(positions: &PointsRefSlice) -> Float { 365 | let sum = positions 366 | .iter() 367 | .circular_tuple_windows::<(_, _)>() 368 | //.take(positions.len() -1) 369 | .fold(Vector::zero(), |sum, position| { 370 | sum + position.0.cross(**position.1) 371 | }); 372 | 373 | average_normal_ref(positions).unwrap().dot(sum).abs() * 0.5 374 | } 375 | 376 | #[inline] 377 | fn _sig_figs(val: Float) -> [u8; 4] { 378 | val.to_ne_bytes() 379 | } 380 | 381 | /* 382 | /// Congruence signature for assigning same colors to congruent faces 383 | #[inline] 384 | pub(crate) fn face_signature(positions: &PointsRefSlice, sensitivity: Float) -> Vec { 385 | let cross_array = positions 386 | .iter() 387 | .circular_tuple_windows::<(_, _, _)>() 388 | .map(|position|{ 389 | position.0.sub(**position.1).cross(position.1.sub(**position.2)).mag() 390 | }) 391 | .collect::>() 392 | .sort_by(|a, b| a - b); 393 | 394 | 395 | let mut cross_array_reversed = cross_array.clone(); 396 | cross_array_reversed.reverse(); 397 | 398 | cross_array 399 | .iter() 400 | .map(|x| sig_figs(x, sensitivity)) 401 | .chain(cross_array_reversed 402 | .iter() 403 | .map(|x| sig_figs(x, sensitivity)) 404 | ) 405 | .collect() 406 | }*/ 407 | 408 | #[inline] 409 | pub(crate) fn orthogonal(v0: &Vector, v1: &Vector, v2: &Vector) -> Vector { 410 | (*v1 - *v0).cross(*v2 - *v1) 411 | } 412 | 413 | #[inline] 414 | pub(crate) fn _are_collinear(v0: &Point, v1: &Point, v2: &Point) -> bool { 415 | orthogonal(v0, v1, v2).mag_sq() < EPSILON 416 | } 417 | 418 | #[inline] 419 | pub(crate) fn _tangent(v0: &Vector, v1: &Vector) -> Vector { 420 | let distance = *v1 - *v0; 421 | *v0 - *v1 * (distance * *v0) / distance.mag_sq() 422 | } 423 | 424 | #[inline] 425 | pub(crate) fn _edge_distance(v1: &Vector, v2: &Vector) -> Float { 426 | _tangent(v1, v2).mag() 427 | } 428 | 429 | #[inline] 430 | pub(crate) fn _average_edge_distance(positions: &PointsRefSlice) -> Float { 431 | positions 432 | .iter() 433 | .circular_tuple_windows::<(_, _)>() 434 | .fold(0.0, |sum, edge_point| { 435 | sum + _edge_distance(edge_point.0, edge_point.1) 436 | }) 437 | / positions.len() as Float 438 | } 439 | 440 | /// Computes the (normalized) normal of a set of an (ordered) set of positions, 441 | /// 442 | /// Tries to do the right thing if the face 443 | /// is non-planar or degenerate. 444 | #[inline] 445 | pub(crate) fn average_normal_ref(positions: &PointsRefSlice) -> Option { 446 | let mut considered_edges = 0; 447 | 448 | let normal = positions 449 | .iter() 450 | .circular_tuple_windows::<(_, _, _)>() 451 | //.take(positions.len() -1) 452 | .fold(Vector::zero(), |normal, corner| { 453 | let ortho_normal = orthogonal(corner.0, corner.1, corner.2); 454 | let mag_sq = ortho_normal.mag_sq(); 455 | // Filter out collinear edge pairs. 456 | if mag_sq < EPSILON as _ { 457 | normal 458 | } else { 459 | // Subtract normalized ortho_normal. 460 | considered_edges += 1; 461 | normal - ortho_normal / mag_sq.sqrt() 462 | } 463 | }); 464 | 465 | if considered_edges != 0 { 466 | Some(normal / considered_edges as f32) 467 | } else { 468 | // Degenerate/zero size face. 469 | //None 470 | 471 | // We just return the normalized vector 472 | // from the origin to the center of the face. 473 | Some(centroid_ref(positions).normalized()) 474 | } 475 | } 476 | 477 | #[inline] 478 | pub(crate) fn _angle_between( 479 | u: &Vector, 480 | v: &Vector, 481 | normal: Option<&Vector>, 482 | ) -> Float { 483 | // Protection against inaccurate computation. 484 | let x = u.normalized().dot(v.normalized()); 485 | let y = if x <= -1.0 { 486 | -1.0 487 | } else if x >= 1.0 { 488 | 1.0 489 | } else { 490 | x 491 | }; 492 | 493 | let angle = y.acos(); 494 | 495 | match normal { 496 | None => angle, 497 | Some(normal) => normal.dot(*u * *v).signum() * angle, 498 | } 499 | } 500 | 501 | #[inline] 502 | pub(crate) fn _minimal_edge_length( 503 | face: &FaceSlice, 504 | positions: &PointsSlice, 505 | ) -> Float { 506 | face_edges(face, positions) 507 | .into_iter() 508 | .fold(Float::NAN, Float::min) 509 | } 510 | 511 | #[inline] 512 | pub(crate) fn _orthogonal_f64(v0: &Point, v1: &Point, v2: &Point) -> DVec3 { 513 | (DVec3::new(v1.x as _, v1.y as _, v1.z as _) 514 | - DVec3::new(v0.x as _, v0.y as _, v0.z as _)) 515 | .cross( 516 | DVec3::new(v2.x as _, v2.y as _, v2.z as _) 517 | - DVec3::new(v1.x as _, v1.y as _, v1.z as _), 518 | ) 519 | } 520 | 521 | #[inline] 522 | pub(crate) fn _are_collinear_f64(v0: &Point, v1: &Point, v2: &Point) -> bool { 523 | _orthogonal_f64(v0, v1, v2).mag_sq() < EPSILON as _ 524 | } 525 | 526 | #[inline] 527 | pub(crate) fn _face_normal_f64(positions: &PointsRefSlice) -> Option { 528 | let mut considered_edges = 0; 529 | 530 | let normal = positions.iter().circular_tuple_windows::<(_, _, _)>().fold( 531 | DVec3::zero(), 532 | |normal, corner| { 533 | considered_edges += 1; 534 | let ortho_normal = _orthogonal_f64(corner.0, corner.1, corner.2); 535 | let mag_sq = ortho_normal.mag_sq(); 536 | // Filter out collinear edge pairs. 537 | if mag_sq < EPSILON as _ { 538 | normal 539 | } else { 540 | // Subtract normalized ortho_normal. 541 | normal - ortho_normal / mag_sq.sqrt() 542 | } 543 | }, 544 | ); 545 | 546 | if considered_edges != 0 { 547 | let n = normal / considered_edges as f64; 548 | Some(Vector::new(n.x as _, n.y as _, n.z as _)) 549 | } else { 550 | // Total degenerate or zero size face. 551 | // We just return the normalized vector 552 | // from the origin to the center of the face. 553 | //Some(centroid_ref(positions).normalized()) 554 | 555 | // FIXME: this branch should return None. 556 | // We need a method to cleanup geometry 557 | // of degenrate faces/edges instead. 558 | None 559 | } 560 | } 561 | 562 | #[inline] 563 | pub(crate) fn vertex_ids_edge_ref_ref<'a>( 564 | entries: &[(&'a Edge, Point)], 565 | offset: VertexKey, 566 | ) -> Vec<(&'a Edge, VertexKey)> { 567 | entries 568 | .par_iter() 569 | .enumerate() 570 | // FIXME swap with next line once rustfmt is fixed. 571 | //.map(|i| (i.1.0, i.0 + offset)) 572 | .map(|i| (entries[i.0].0, i.0 as VertexKey + offset)) 573 | .collect() 574 | } 575 | 576 | #[inline] 577 | pub(crate) fn vertex_ids_ref_ref<'a>( 578 | entries: &[(&'a FaceSlice, Point)], 579 | offset: VertexKey, 580 | ) -> Vec<(&'a FaceSlice, VertexKey)> { 581 | entries 582 | .par_iter() 583 | .enumerate() 584 | // FIXME swap with next line once rustfmt is fixed. 585 | //.map(|i| (i.1.0, i.0 + offset)) 586 | .map(|i| (entries[i.0].0, i.0 as VertexKey + offset)) 587 | .collect() 588 | } 589 | 590 | #[allow(clippy::needless_lifetimes)] 591 | #[inline] 592 | pub(crate) fn vertex_ids_ref<'a>( 593 | entries: &'a [(Face, Point)], 594 | offset: VertexKey, 595 | ) -> Vec<(&'a FaceSlice, VertexKey)> { 596 | entries 597 | .par_iter() 598 | .enumerate() 599 | // FIXME swap with next line once rustfmt is fixed. 600 | //.map(|i| (i.1.0, i.0 + offset)) 601 | .map(|i| (entries[i.0].0.as_slice(), i.0 as VertexKey + offset)) 602 | .collect() 603 | } 604 | 605 | #[inline] 606 | pub(crate) fn _vertex_ids( 607 | entries: &[(Face, Point)], 608 | offset: VertexKey, 609 | ) -> Vec<(Face, VertexKey)> { 610 | entries 611 | .par_iter() 612 | .enumerate() 613 | // FIXME swap with next line once rustfmt is fixed. 614 | //.map(|i| (i.1.0, i.0 + offset)) 615 | .map(|i| (entries[i.0].0.clone(), i.0 as VertexKey + offset)) 616 | .collect() 617 | } 618 | 619 | #[inline] 620 | pub(crate) fn vertex( 621 | key: &FaceSlice, 622 | entries: &[(&FaceSlice, VertexKey)], 623 | ) -> Option { 624 | entries 625 | .par_iter() 626 | .find_first(|f| key == f.0) 627 | .map(|entry| entry.1) 628 | } 629 | 630 | #[inline] 631 | pub(crate) fn vertex_point<'a>( 632 | key: &FaceSlice, 633 | entries: &'a [(&FaceSlice, Point)], 634 | ) -> Option<&'a Point> { 635 | entries 636 | .par_iter() 637 | .find_first(|f| key == f.0) 638 | .map(|entry| &entry.1) 639 | } 640 | 641 | #[inline] 642 | pub(crate) fn vertex_edge( 643 | key: &Edge, 644 | entries: &[(&Edge, VertexKey)], 645 | ) -> Option { 646 | entries 647 | .par_iter() 648 | .find_first(|f| key == f.0) 649 | .map(|entry| entry.1) 650 | } 651 | 652 | #[inline] 653 | pub(crate) fn vertex_edge_point<'a>( 654 | key: &Edge, 655 | entries: &'a [(&Edge, Point)], 656 | ) -> Option<&'a Point> { 657 | entries 658 | .par_iter() 659 | .find_first(|f| key == f.0) 660 | .map(|entry| &entry.1) 661 | } 662 | 663 | #[inline] 664 | pub(crate) fn vertex_values_as_ref(entries: &[(T, Point)]) -> Vec<&Point> { 665 | entries.iter().map(|e| &e.1).collect() 666 | } 667 | 668 | pub(crate) fn vertex_values(entries: &[(T, Point)]) -> Points { 669 | entries.iter().map(|e| e.1).collect() 670 | } 671 | 672 | #[inline] 673 | pub(crate) fn face_arity_matches( 674 | face: &FaceSlice, 675 | face_arity_mask: Option<&[usize]>, 676 | ) -> bool { 677 | face_arity_mask.map_or_else( 678 | || true, 679 | |face_arity_mask| face_arity_mask.contains(&face.len()), 680 | ) 681 | } 682 | 683 | #[inline] 684 | pub(crate) fn face_centers( 685 | face_index: &FacesSlice, 686 | positions: &PointsSlice, 687 | ) -> Points { 688 | face_index 689 | .iter() 690 | .map(|face| centroid_ref(&index_as_positions(face, positions))) 691 | .collect() 692 | } 693 | 694 | #[inline] 695 | pub(crate) fn reciprocal(vector: &Vector) -> Vector { 696 | *vector / vector.mag_sq() 697 | } 698 | 699 | #[inline] 700 | pub(crate) fn reciprocate_face_centers( 701 | face_index: &FacesSlice, 702 | positions: &PointsSlice, 703 | ) -> Points { 704 | face_centers(face_index, positions) 705 | .iter() 706 | .map(reciprocal) 707 | .collect() 708 | } 709 | 710 | #[inline] 711 | pub(crate) fn _reciprocate_faces( 712 | face_index: &FacesSlice, 713 | positions: &PointsSlice, 714 | ) -> Points { 715 | face_index 716 | .iter() 717 | .map(|face| { 718 | let face_positions = index_as_positions(face, positions); 719 | let centroid = centroid_ref(&face_positions); 720 | let normal = average_normal_ref(&face_positions).unwrap(); 721 | let c_dot_n = centroid.dot(normal); 722 | let edge_distance = _average_edge_distance(&face_positions); 723 | reciprocal(&(normal * c_dot_n)) * (1.0 + edge_distance) * 0.5 724 | }) 725 | .collect() 726 | } 727 | 728 | #[inline] 729 | pub(crate) fn _distinct_edges(faces: &FacesSlice) -> Edges { 730 | faces 731 | .iter() 732 | .flat_map(|face| { 733 | face.iter() 734 | // Grab two index entries. 735 | .circular_tuple_windows::<(_, _)>() 736 | .filter(|t| t.0 < t.1) 737 | // Create an edge from them. 738 | .map(|t| [*t.0, *t.1]) 739 | .collect::>() 740 | }) 741 | .unique() 742 | .collect() 743 | } 744 | -------------------------------------------------------------------------------- /src/io/bevy.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use bevy::render::{ 3 | mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}, 4 | render_asset::RenderAssetUsages, 5 | }; 6 | 7 | /// Conversion to a bevy [`Mesh`]. 8 | impl From for Mesh { 9 | fn from(mut polyhedron: Polyhedron) -> Self { 10 | polyhedron.reverse(); 11 | 12 | let (index, positions, normals) = polyhedron.to_triangle_mesh_buffers(); 13 | 14 | let mut mesh = Mesh::new( 15 | PrimitiveTopology::TriangleList, 16 | RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, 17 | ); 18 | 19 | mesh.insert_indices(Indices::U32(index)); 20 | 21 | mesh.insert_attribute( 22 | Mesh::ATTRIBUTE_POSITION, 23 | VertexAttributeValues::Float32x3( 24 | positions 25 | .par_iter() 26 | .map(|p| [p.x, p.y, p.z]) 27 | .collect::>(), 28 | ), 29 | ); 30 | 31 | mesh.insert_attribute( 32 | Mesh::ATTRIBUTE_NORMAL, 33 | VertexAttributeValues::Float32x3( 34 | normals 35 | .par_iter() 36 | .map(|n| [-n.x, -n.y, -n.z]) 37 | .collect::>(), 38 | ), 39 | ); 40 | 41 | mesh 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "bevy")] 2 | mod bevy; 3 | 4 | #[cfg(feature = "obj")] 5 | mod obj; 6 | 7 | /// OBJ writing/loading. 8 | #[cfg(feature = "nsi")] 9 | mod nsi; 10 | -------------------------------------------------------------------------------- /src/io/nsi.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use nsi_core as nsi; 3 | 4 | /// Conversion to [ɴsɪ](https:://crates.io/crates/nsi). 5 | impl<'a> Polyhedron { 6 | /// Sends the polyhedron to the specified 7 | /// [ɴsɪ](https:://crates.io/crates/nsi) context. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `handle` – Handle of the node being created. If omitted, the name of 12 | /// the polyhedron will be used as a handle. 13 | /// 14 | /// * `crease_hardness` - The hardness of edges (default: 10). 15 | /// 16 | /// * `corner_hardness` - The hardness of vertices (default: 0). 17 | /// 18 | /// * `auto_corners` - Whether to create corners where more than two edges 19 | /// meet. When set to `true` these automatically form a hard corner with 20 | /// the same hardness as `crease_hardness`. This is ignored if 21 | /// `corner_hardness` is `Some`. 22 | /// 23 | /// This activates the specific *deRose* extensions for the Catmull-Clark 24 | /// subdivision creasing algorithm. See fig. 8c/d in 25 | /// [this paper](http://graphics.pixar.com/people/derose/publications/Geri/paper.pdf). 26 | #[cfg(feature = "nsi")] 27 | pub fn to_nsi( 28 | &self, 29 | ctx: &nsi::Context, 30 | handle: Option<&str>, 31 | crease_hardness: Option, 32 | corner_hardness: Option, 33 | auto_corners: Option, 34 | ) -> String { 35 | let handle = handle.unwrap_or(self.name.as_str()).to_string(); 36 | // Create a new mesh node. 37 | ctx.create(&handle, nsi::node::MESH, None); 38 | 39 | // Flatten point vector. 40 | let position = unsafe { 41 | std::slice::from_raw_parts( 42 | self.positions.as_ptr().cast::(), 43 | self.positions_len() * 3, 44 | ) 45 | }; 46 | 47 | ctx.set_attribute( 48 | &handle, 49 | &[ 50 | // Positions. 51 | nsi::points!("P", position), 52 | // VertexKey into the position array. 53 | nsi::integers!( 54 | "P.indices", 55 | bytemuck::cast_slice( 56 | &self 57 | .face_index 58 | .par_iter() 59 | .flat_map(|face| face.clone()) 60 | .collect::>() 61 | ) 62 | ), 63 | // Arity of each face. 64 | nsi::integers!( 65 | "nvertices", 66 | &self 67 | .face_index 68 | .par_iter() 69 | .map(|face| face.len() as i32) 70 | .collect::>() 71 | ), 72 | // Render this as a C-C subdivison surface. 73 | nsi::string!("subdivision.scheme", "catmull-clark"), 74 | // This saves us from having to reverse the mesh ourselves. 75 | nsi::integer!("clockwisewinding", true as _), 76 | ], 77 | ); 78 | 79 | // Default: semi sharp creases. 80 | let crease_hardness = crease_hardness.unwrap_or(10.); 81 | 82 | // Crease each of our edges a bit? 83 | if 0.0 != crease_hardness { 84 | let edges = self 85 | .to_edges() 86 | .into_iter() 87 | .flat_map(|edge| edge) 88 | .collect::>(); 89 | ctx.set_attribute( 90 | &handle, 91 | &[ 92 | nsi::integers!( 93 | "subdivision.creasevertices", 94 | bytemuck::cast_slice(&edges) 95 | ), 96 | nsi::floats!( 97 | "subdivision.creasesharpness", 98 | &vec![crease_hardness; edges.len() / 2] 99 | ), 100 | ], 101 | ); 102 | } 103 | 104 | match corner_hardness { 105 | Some(hardness) => { 106 | if 0.0 < hardness { 107 | let corners = self 108 | .positions 109 | .par_iter() 110 | .enumerate() 111 | .map(|(i, _)| i as i32) 112 | .collect::>(); 113 | ctx.set_attribute( 114 | &handle, 115 | &[ 116 | nsi::integers!( 117 | "subdivision.cornervertices", 118 | &corners 119 | ), 120 | nsi::floats!( 121 | "subdivision.cornersharpness", 122 | &vec![hardness; corners.len()] 123 | ), 124 | ], 125 | ); 126 | } 127 | } 128 | 129 | // Have the renderer semi create sharp corners automagically. 130 | None => ctx.set_attribute( 131 | &handle, 132 | &[ 133 | // Disabling below flag activates the specific 134 | // deRose extensions for the C-C creasing algorithm 135 | // that causes any vertex where more then three 136 | // creased edges meet to form a corner. 137 | // See fig. 8c/d in this paper: 138 | // http://graphics.pixar.com/people/derose/publications/Geri/paper.pdf 139 | nsi::integer!( 140 | "subdivision.smoothcreasecorners", 141 | !auto_corners.unwrap_or(true) as _ 142 | ), 143 | ], 144 | ), 145 | }; 146 | 147 | handle 148 | } 149 | 150 | /// Creates the buffers to send a polyhedron to an 151 | /// [ɴsɪ](https:://crates.io/crates/nsi) context. 152 | /// 153 | /// # Arguments 154 | /// 155 | /// * `crease_hardness` - The hardness of edges (default: 10). 156 | /// 157 | /// * `corner_hardness` - The hardness of vertices (default: 0). 158 | /// 159 | /// # Examples 160 | /// 161 | /// ``` 162 | /// # use nsi::*; 163 | /// # use polyhedron_ops::Polyhedron; 164 | /// # let ctx = Context::new(None).unwrap(); 165 | /// # let polyhedron = Polyhedron::dodecahedron().chamfer(None, true).propellor(None, true).finalize(); 166 | /// let ( 167 | /// position, 168 | /// position_index, 169 | /// face_arity, 170 | /// crease_index, 171 | /// crease_hardness, 172 | /// corner_index, 173 | /// corner_hardness, 174 | /// ) = polyhedron.to_nsi(Some(10.0), Some(5.0)); 175 | /// 176 | /// ctx.create("polyhedron", nsi::node::MESH, None); 177 | /// 178 | /// ctx.set_attribute( 179 | /// "polyhedron", 180 | /// &[ 181 | /// nsi::points!("P", position), 182 | /// nsi::integers!("P.indices", &position_index), 183 | /// nsi::integers!("nvertices", &face_arity), 184 | /// nsi::integers!("subdivision.creasevertices", &crease_index.unwrap()), 185 | /// nsi::floats!("subdivision.creasehardness", &crease_hardness.unwrap()), 186 | /// nsi::integers!("subdivision.cornervertices", &corner_index.unwrap()), 187 | /// nsi::floats!("subdivision.cornerhardness", &corner_hardness.unwrap()), 188 | /// nsi::integer!("clockwisewinding", 1) 189 | /// ] 190 | /// ); 191 | /// ``` 192 | #[cfg(feature = "nsi")] 193 | pub fn to_nsi_buffers( 194 | &'a self, 195 | crease_hardness: Option, 196 | corner_hardness: Option, 197 | ) -> ( 198 | &'a [[f32; 3]], 199 | Vec, 200 | Vec, 201 | Option>, 202 | Option>, 203 | Option>, 204 | Option>, 205 | ) { 206 | // Flatten point vector. 207 | let position = unsafe { 208 | std::slice::from_raw_parts( 209 | self.positions.as_ptr().cast::<[f32; 3]>(), 210 | self.positions_len(), 211 | ) 212 | }; 213 | 214 | let face_index = self 215 | .face_index 216 | .par_iter() 217 | .flat_map(|face| bytemuck::cast_slice(face).to_vec()) 218 | .collect::>(); 219 | 220 | let face_arity = self 221 | .face_index 222 | .par_iter() 223 | .map(|face| face.len() as i32) 224 | .collect::>(); 225 | 226 | let (crease_index, crease_hardness) = 227 | if let Some(crease_hardness) = crease_hardness { 228 | let edge = self 229 | .to_edges() 230 | .into_iter() 231 | .flat_map(|edge| edge) 232 | .collect::>(); 233 | 234 | let edge_len = edge.len(); 235 | ( 236 | Some(bytemuck::cast_vec(edge)), 237 | Some(vec![crease_hardness; edge_len / 2]), 238 | ) 239 | } else { 240 | (None, None) 241 | }; 242 | 243 | let (corner_index, corner_hardness) = 244 | if let Some(corner_hardness) = corner_hardness { 245 | let corner = self 246 | .positions 247 | .par_iter() 248 | .enumerate() 249 | .map(|(i, _)| i as i32) 250 | .collect::>(); 251 | 252 | let corner_len = corner.len(); 253 | (Some(corner), Some(vec![corner_hardness; corner_len])) 254 | } else { 255 | (None, None) 256 | }; 257 | 258 | ( 259 | position, 260 | face_index, 261 | face_arity, 262 | crease_index, 263 | crease_hardness, 264 | corner_index, 265 | corner_hardness, 266 | ) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/io/obj.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::{ 3 | error::Error, 4 | fs::File, 5 | io::Write as IoWrite, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | impl Polyhedron { 10 | /// Write the polyhedron to a 11 | /// [Wavefront OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file) 12 | /// file. 13 | /// 14 | /// The [`name`](Polyhedron::name()) of the polyhedron is appended 15 | /// to the given `destination` and postfixed with the extension 16 | /// `.obj`. 17 | /// 18 | /// Depending on the target coordinate system (left- or right 19 | /// handed) the mesh’s winding order can be reversed with the 20 | /// `reverse_winding` flag. 21 | /// 22 | /// The return value, on success, is the final, complete path of 23 | /// the OBJ file. 24 | #[cfg(feature = "obj")] 25 | pub fn write_obj( 26 | &self, 27 | destination: &Path, 28 | reverse_winding: bool, 29 | ) -> Result> { 30 | let path = destination.join(format!("polyhedron-{}.obj", self.name)); 31 | let mut file = File::create(path.clone())?; 32 | 33 | writeln!(file, "o {}", self.name)?; 34 | 35 | for vertex in &self.positions { 36 | writeln!(file, "v {} {} {}", vertex.x, vertex.y, vertex.z)?; 37 | } 38 | 39 | match reverse_winding { 40 | true => { 41 | for face in &self.face_index { 42 | write!(file, "f")?; 43 | for vertex_index in face.iter().rev() { 44 | write!(file, " {}", vertex_index + 1)?; 45 | } 46 | writeln!(file)?; 47 | } 48 | } 49 | false => { 50 | for face in &self.face_index { 51 | write!(file, "f")?; 52 | for vertex_index in face { 53 | write!(file, " {}", vertex_index + 1)?; 54 | } 55 | writeln!(file)?; 56 | } 57 | } 58 | }; 59 | 60 | file.flush()?; 61 | 62 | Ok(path) 63 | } 64 | 65 | pub fn read_obj( 66 | source: &Path, 67 | reverse_winding: bool, 68 | ) -> Result { 69 | let (geometry, _) = 70 | tobj::load_obj(source, &tobj::OFFLINE_RENDERING_LOAD_OPTIONS)?; 71 | 72 | Ok(Polyhedron { 73 | face_index: { 74 | let mut index = 0; 75 | geometry[0] 76 | .mesh 77 | .face_arities 78 | .iter() 79 | .map(|&face_arity| { 80 | assert!(0 != face_arity); 81 | let face_arity = face_arity as usize; 82 | let mut face_indices = geometry[0].mesh.indices 83 | [index..index + face_arity] 84 | .to_vec(); 85 | if reverse_winding { 86 | face_indices.reverse(); 87 | } 88 | index += face_arity; 89 | 90 | face_indices 91 | }) 92 | .collect() 93 | }, 94 | positions: geometry[0] 95 | .mesh 96 | .positions 97 | .iter() 98 | .array_chunks::<3>() 99 | .map(|p| Point::new(*p[0], *p[1], *p[2])) 100 | .collect(), 101 | name: geometry[0].name.clone(), 102 | ..Default::default() 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::many_single_char_names)] 2 | #![feature(iter_array_chunks)] 3 | //! # Conway-Hart Polyhedron Operations 4 | //! 5 | //! This crate implements the [Conway Polyhedron 6 | //! Operators](https://en.wikipedia.org/wiki/Conway_polyhedron_notation) 7 | //! and their extensions by [George W. Hart](http://www.georgehart.com/) and others. 8 | //! 9 | //! The internal representation uses *n*-gon mesh buffers. These need 10 | //! preprocessing before they can be sent to a GPU but are almost fine to send 11 | //! to an offline renderer, as-is. 12 | //! 13 | //! See the `playground` example for code on how to do either. 14 | //! 15 | //! ## Example 16 | //! ``` 17 | //! use polyhedron_ops::Polyhedron; 18 | //! use std::path::Path; 19 | //! 20 | //! // Conway notation: gapcD 21 | //! let polyhedron = Polyhedron::dodecahedron() 22 | //! .chamfer(None, true) 23 | //! .propellor(None, true) 24 | //! .ambo(None, true) 25 | //! .gyro(None, None, true) 26 | //! .finalize(); 27 | //! 28 | //! // Export as ./polyhedron-gapcD.obj 29 | //! # #[cfg(feature = "obj")] 30 | //! # { 31 | //! polyhedron.write_obj(&Path::new("."), false); 32 | //! # } 33 | //! ``` 34 | //! The above code starts from a [dodecahedron](https://en.wikipedia.org/wiki/Dodecahedron) 35 | //! and iteratively applies four operators. 36 | //! 37 | //! The resulting shape is shown below. 38 | //! 39 | //! ![](https://raw.githubusercontent.com/virtualritz/polyhedron-operators/HEAD/gapcD.jpg) 40 | //! 41 | //! ## Cargo Features 42 | //! 43 | //! ```toml 44 | //! [dependencies] 45 | //! polyhedron-ops = { version = "0.3", features = [ "bevy", "nsi", "obj", "parser" ] } 46 | //! ``` 47 | //! 48 | //! * `bevy` – Add support for converting a polyhedron into a [`bevy`](https://bevyengine.org/) 49 | //! [`Mesh`](https://docs.rs/bevy/latest/bevy/render/mesh/struct.Mesh.html). 50 | //! See the `bevy` example. 51 | //! 52 | //! * `nsi` – Add support for sending data to renderers implementing the [ɴsɪ](https://crates.io/crates/nsi/) 53 | //! API. The function is called [`to_nsi()`](Polyhedron::to_nsi()). 54 | //! 55 | //! * `obj` – Add support for output to [Wavefront OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file) 56 | //! via the [`write_obj()`](Polyhedron::write_obj()) function. 57 | //! 58 | //! * `parser` – Add support for parsing strings in [Conway Polyhedron Notation](https://en.wikipedia.org/wiki/Conway_polyhedron_notation). 59 | //! This feature implements 60 | //! [`Polyhedron::TryFrom<&str>`](Polyhedron::try_from<&str>). 61 | use crate::helpers::*; 62 | use itertools::Itertools; 63 | use rayon::prelude::*; 64 | use ultraviolet as uv; 65 | #[cfg(feature = "parser")] 66 | #[macro_use] 67 | extern crate pest_derive; 68 | 69 | mod base_polyhedra; 70 | mod helpers; 71 | mod io; 72 | mod mesh_buffers; 73 | mod operators; 74 | #[cfg(feature = "parser")] 75 | mod parser; 76 | mod selection; 77 | mod text_helpers; 78 | 79 | #[cfg(test)] 80 | mod tests; 81 | 82 | static EPSILON: f32 = 0.00000001; 83 | 84 | pub type Float = f32; 85 | pub type VertexKey = u32; 86 | pub type FaceKey = u32; 87 | pub type Face = Vec; 88 | pub(crate) type FaceSlice = [VertexKey]; 89 | pub type Faces = Vec; 90 | pub(crate) type FacesSlice = [Face]; 91 | pub type FaceSet = Vec; 92 | pub type Edge = [VertexKey; 2]; 93 | pub type Edges = Vec; 94 | pub type EdgesSlice = [Edge]; 95 | pub(crate) type _EdgeSlice = [Edge]; 96 | pub type Point = uv::vec::Vec3; 97 | pub type Vector = uv::vec::Vec3; 98 | pub type Normal = Vector; 99 | #[allow(dead_code)] 100 | pub type Normals = Vec; 101 | pub type Points = Vec; 102 | pub(crate) type PointsSlice = [Point]; 103 | pub(crate) type PointsRefSlice<'a> = [&'a Point]; 104 | 105 | #[derive(Clone, Debug)] 106 | pub struct Polyhedron { 107 | face_index: Faces, 108 | positions: Points, 109 | name: String, 110 | // This stores a FaceSet for each 111 | // set of faces belonging to the 112 | // same operations. 113 | face_set_index: Vec, 114 | } 115 | 116 | impl Default for Polyhedron { 117 | fn default() -> Self { 118 | Self::new() 119 | } 120 | } 121 | 122 | #[cfg(feature = "tilings")] 123 | use tilings::RegularTiling; 124 | #[cfg(feature = "tilings")] 125 | impl From for Polyhedron { 126 | fn from(rt: RegularTiling) -> Polyhedron { 127 | Polyhedron { 128 | positions: rt 129 | .positions() 130 | .iter() 131 | .map(|p| Point::new(p.x, 0.0, p.y)) 132 | .collect(), 133 | face_index: rt.faces().clone(), 134 | face_set_index: Vec::new(), 135 | name: rt.name().to_string(), 136 | } 137 | } 138 | } 139 | 140 | #[cfg(feature = "tilings")] 141 | use tilings::SemiRegularTiling; 142 | #[cfg(feature = "tilings")] 143 | impl From for Polyhedron { 144 | fn from(rt: SemiRegularTiling) -> Polyhedron { 145 | Polyhedron { 146 | positions: rt 147 | .positions() 148 | .iter() 149 | .map(|p| Point::new(p.x, 0.0, p.y)) 150 | .collect(), 151 | face_index: rt.faces().clone(), 152 | face_set_index: Vec::new(), 153 | name: rt.name().to_string(), 154 | } 155 | } 156 | } 157 | 158 | impl Polyhedron { 159 | #[inline] 160 | pub fn new() -> Self { 161 | Self { 162 | positions: Vec::new(), 163 | face_index: Vec::new(), 164 | face_set_index: Vec::new(), 165 | name: String::new(), 166 | } 167 | } 168 | 169 | #[inline] 170 | pub fn from( 171 | name: &str, 172 | positions: Points, 173 | face_index: Faces, 174 | face_set_index: Option>, 175 | ) -> Self { 176 | Self { 177 | positions, 178 | face_index, 179 | face_set_index: face_set_index.unwrap_or_default(), 180 | name: name.to_string(), 181 | } 182 | } 183 | 184 | /// Returns the axis-aligned bounding box of the polyhedron in the format 185 | /// `[x_min, y_min, z_min, x_max, y_max, z_max]`. 186 | #[inline] 187 | pub fn bounding_box(&self) -> [f64; 6] { 188 | let mut bounds = [0.0f64; 6]; 189 | self.positions.iter().for_each(|point| { 190 | if bounds[0] > point.x as _ { 191 | bounds[0] = point.x as _; 192 | } else if bounds[3] < point.x as _ { 193 | bounds[3] = point.x as _; 194 | } 195 | 196 | if bounds[1] > point.y as _ { 197 | bounds[1] = point.y as _; 198 | } else if bounds[4] < point.y as _ { 199 | bounds[4] = point.y as _; 200 | } 201 | 202 | if bounds[2] > point.z as _ { 203 | bounds[2] = point.z as _; 204 | } else if bounds[5] < point.z as _ { 205 | bounds[5] = point.z as _; 206 | } 207 | }); 208 | bounds 209 | } 210 | 211 | /// Reverses the winding order of faces. 212 | /// 213 | /// Clockwise(default) becomes counter-clockwise and vice versa. 214 | pub fn reverse(&mut self) -> &mut Self { 215 | self.face_index 216 | .par_iter_mut() 217 | .for_each(|face| face.reverse()); 218 | 219 | self 220 | } 221 | 222 | /// Returns the name of this polyhedron. This can be used to reconstruct the 223 | /// polyhedron using `Polyhedron::from<&str>()`. 224 | #[inline] 225 | pub fn name(&self) -> &String { 226 | &self.name 227 | } 228 | 229 | #[inline] 230 | pub(crate) fn positions_len(&self) -> usize { 231 | self.positions.len() 232 | } 233 | 234 | #[inline] 235 | pub fn positions(&self) -> &Points { 236 | &self.positions 237 | } 238 | 239 | pub fn faces(&self) -> &Faces { 240 | &self.face_index 241 | } 242 | 243 | // Resizes the polyhedron to fit inside a unit sphere. 244 | #[inline] 245 | pub fn normalize(&mut self) -> &mut Self { 246 | max_resize(&mut self.positions, 1.); 247 | self 248 | } 249 | 250 | /// Compute the edges of the polyhedron. 251 | #[inline] 252 | pub fn to_edges(&self) -> Edges { 253 | let edges = self 254 | .face_index 255 | .par_iter() 256 | .map(|face| { 257 | face.iter() 258 | // Grab two index entries. 259 | .circular_tuple_windows::<(_, _)>() 260 | .filter(|t| t.0 < t.1) 261 | // Create an edge from them. 262 | .map(|t| [*t.0, *t.1]) 263 | .collect::>() 264 | }) 265 | .flatten() 266 | .collect::>(); 267 | 268 | edges.into_iter().unique().collect() 269 | } 270 | 271 | /// Compute the edges of the polyhedron. 272 | #[inline] 273 | pub fn to_edges_filtered( 274 | &self, 275 | face_arity_mask: Option<&[usize]>, 276 | face_index_mask: Option<&[FaceKey]>, 277 | regular_faces_only: Option, 278 | ) -> Edges { 279 | use crate::selection::is_face_selected; 280 | 281 | let edges = self 282 | .face_index 283 | .iter() 284 | .enumerate() 285 | .filter_map(|(index, face)| { 286 | if is_face_selected( 287 | face, 288 | index, 289 | &self.positions, 290 | face_arity_mask, 291 | face_index_mask, 292 | regular_faces_only, 293 | ) { 294 | Some( 295 | face.iter() 296 | // Grab two index entries. 297 | .circular_tuple_windows::<(_, _)>() 298 | .filter(|t| t.0 < t.1) 299 | // Create an edge from them. 300 | .map(|t| [*t.0, *t.1]) 301 | .collect::>(), 302 | ) 303 | } else { 304 | None 305 | } 306 | }) 307 | .flatten() 308 | .collect::>(); 309 | 310 | edges.into_iter().unique().collect() 311 | } 312 | 313 | /// Turns the builder into a final object. 314 | pub fn finalize(&self) -> Self { 315 | self.clone() 316 | } 317 | } 318 | 319 | impl Polyhedron { 320 | /// Appends indices for newly added faces as a new [`FaceSet`] to the 321 | /// [`FaceSetIndex`]. 322 | #[inline] 323 | fn append_new_face_set(&mut self, size: usize) { 324 | self.face_set_index 325 | .append(&mut vec![((self.face_index.len() as VertexKey) 326 | ..((self.face_index.len() + size) as VertexKey)) 327 | .collect()]); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/mesh_buffers.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// # Mesh Buffer (GPU/realtime) Helpers 4 | impl Polyhedron { 5 | /// Returns a flat [`u32`] triangle index buffer and two matching point and 6 | /// normal buffers. 7 | /// 8 | /// This is mostly useful for realtime rendering, e.g. sending data to a 9 | /// GPU. 10 | /// 11 | /// All the faces are disconnected. I.e. positions & normals are duplicated 12 | /// for each shared vertex. 13 | pub fn to_triangle_mesh_buffers(&self) -> (Vec, Points, Normals) { 14 | let (positions, normals): (Vec<_>, Vec<_>) = self 15 | .face_index 16 | .par_iter() 17 | .flat_map(|face| { 18 | face.iter() 19 | // Cycle forever. 20 | .cycle() 21 | // Start at 3-tuple belonging to the 22 | // face's last vertex. 23 | .skip(face.len() - 1) 24 | // Grab the next three vertex index 25 | // entries. 26 | .tuple_windows::<(_, _, _)>() 27 | .take(face.len()) 28 | .map(|t| { 29 | // The middle point of out tuple 30 | let point = self.positions[*t.1 as usize]; 31 | // Create a normal from that 32 | let normal = -orthogonal( 33 | &self.positions[*t.0 as usize], 34 | &point, 35 | &self.positions[*t.2 as usize], 36 | ); 37 | let mag_sq = normal.mag_sq(); 38 | 39 | ( 40 | point, 41 | // Check for collinearity: 42 | if mag_sq < EPSILON as _ { 43 | average_normal_ref(&index_as_positions( 44 | face, 45 | self.positions(), 46 | )) 47 | .unwrap() 48 | } else { 49 | normal / mag_sq.sqrt() 50 | }, 51 | ) 52 | }) 53 | // For each vertex of the face. 54 | .collect::>() 55 | }) 56 | .unzip(); 57 | 58 | // Build a new face index. Same topology as the old one, only with new 59 | // keys. 60 | let triangle_face_index = self 61 | .face_index 62 | .iter() 63 | // Build a new index where each face has the original arity and the 64 | // new numbering. 65 | .scan(0.., |counter, face| { 66 | Some(counter.take(face.len()).collect::>()) 67 | }) 68 | // Now split each of these faces into triangles. 69 | .flat_map(|face| match face.len() { 70 | // Filter out degenerate faces. 71 | 1 | 2 => vec![], 72 | // Bitriangulate quadrilateral faces use shortest diagonal so 73 | // triangles are most nearly equilateral. 74 | 4 => { 75 | let p = index_as_positions(face.as_slice(), &positions); 76 | 77 | if (*p[0] - *p[2]).mag_sq() < (*p[1] - *p[3]).mag_sq() { 78 | vec![ 79 | face[0], face[1], face[2], face[0], face[2], 80 | face[3], 81 | ] 82 | } else { 83 | vec![ 84 | face[1], face[2], face[3], face[1], face[3], 85 | face[0], 86 | ] 87 | } 88 | } 89 | 5 => vec![ 90 | face[0], face[1], face[4], face[1], face[2], face[4], 91 | face[4], face[2], face[3], 92 | ], 93 | // FIXME: a nicer way to triangulate n-gons. 94 | _ => { 95 | let a = face[0]; 96 | let mut bb = face[1]; 97 | face.iter() 98 | .skip(2) 99 | .flat_map(|c| { 100 | let b = bb; 101 | bb = *c; 102 | vec![a, b, *c] 103 | }) 104 | .collect() 105 | } 106 | }) 107 | .collect(); 108 | 109 | (triangle_face_index, positions, normals) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use pest::{iterators::Pairs, Parser}; 3 | use std::{fmt::Debug, str::FromStr}; 4 | 5 | #[derive(Parser)] 6 | #[grammar = "grammar.pest"] 7 | struct ConwayPolyhedronNotationParser; 8 | 9 | impl TryFrom<&str> for Polyhedron { 10 | type Error = pest::error::Error; 11 | 12 | /// Tries to create a polyhedron from a [Conway Polyedron Notation](https://en.wikipedia.org/wiki/Conway_polyhedron_notation) 13 | /// string. 14 | /// 15 | /// E.g. the string `aD` creates a 16 | /// [dodecahedron](Polyhedron::dodecahedron()) 17 | /// with an [ambo](Polyhedron::ambo()) operation applied to it. Also known 18 | /// as an [icosidodecahedron](https://en.wikipedia.org/wiki/Icosidodecahedron). 19 | /// One of the [Archimedian solids](https://en.wikipedia.org/wiki/Archimedean_solid). 20 | /// 21 | /// # Overview 22 | /// 23 | /// * All parameters are optional (i.e. have defaults, if absent). 24 | /// 25 | /// * Any (number of) parameter(s) can be skipped by using commata (`,`). 26 | /// 27 | /// * Whitespace is ignored. This includes tabs, newlines and carriage 28 | /// returns. 29 | /// 30 | /// # Tokens 31 | /// 32 | /// **Integers** are written as decimal numbers. 33 | /// 34 | /// **Floats** are written as decimal numbers with an optional decimal 35 | /// point and an optional exponent. 36 | /// 37 | /// **Booleans** are written as `{t}` (true) or `{f}` (false). 38 | /// 39 | /// Parameter names in the operator list below are prefixed with the 40 | /// expected type: 41 | /// 42 | /// * `b_` – boolean 43 | /// * `f_` – float 44 | /// * `uf_` – unsigned float 45 | /// * `i_` – integer 46 | /// * `ui_` – unsigned integer 47 | /// * `[ui_, …]` – array of unsigned integers or single unsigned integer 48 | /// 49 | /// ## Platonic Solids 50 | /// 51 | /// * `T` – tetrahedron 52 | /// * `C` – hexahedron (cube) 53 | /// * `O` – octahedron 54 | /// * `D` – dodecahedron 55 | /// * `I` – icosahedron 56 | /// 57 | /// ## Prisms & Antiprisms 58 | /// 59 | /// * `P` *`ui_number`* – prism with resp. number of sides 60 | /// * `A` *`ui_number`* – antiprism with resp. number of sides 61 | /// 62 | /// ## Operators 63 | /// 64 | /// * `a` *`uf_ratio`* – ambo 65 | /// * `b` *`f_ratio`*, *`f_height`*, *`[ui_vertex_valence_mask, …]`*, 66 | /// *`b_regular_faces_only`* – bevel (equiv. to `ta`) 67 | /// * `c` *`uf_ratio`* – chamfer 68 | /// * `d` – dual 69 | /// * `e` *`uf_ratio`* – expand (a.k.a. explode, equiv. to `aa`) 70 | /// * `g` *`uf_ratio`*, *`f_height`* – gyro 71 | /// * `i` *`f_offset`* – inset/loft (equiv. to `x,N`) 72 | /// * `j` *`uf_ratio`* – join (equiv. to `dad`) 73 | /// * `K` *`ui_iterations`* – planarize (quick & dirty canonicalization) 74 | /// * `k` *`f_height`*, *`[ui_face_arity_mask, …]`*, *`[ui_face_index_mask, 75 | /// ]`*, *`b_regular_faces_only`* – kis 76 | /// * `M` *`uf_ratio`*, *`f_height`*, *`[ui_vertex_valence_mask, …]`*, 77 | /// *`b_regular_faces_only`* – medial (equiv. to `dta`) 78 | /// * `m` *`uf_ratio`*, *`f_height`*, *`[ui_vertex_valence_mask, …]`*, 79 | /// *`b_regular_faces_only`* – meta (equiv. to `k,,3j`) 80 | /// * `n` *`f_height`*, *`[ui_vertex_valence_mask, …]`*, 81 | /// *`b_regular_faces_only`* – needle (equiv. to `dt`) 82 | /// * `o` *`uf_ratio`* – ortho (equiv. to `jj`) 83 | /// * `p` *`uf_ratio`* – propellor 84 | /// * `q` *`f_height`* – quinto 85 | /// * `r` – reflect 86 | /// * `S` *`uf_strength`* – spherize 87 | /// * `s` *`uf_ratio`*, *`f_height`* – snub (equiv. to `dgd`) 88 | /// * `t` *`f_height`*, *`[ui_vertex_valence_mask, …]`*, 89 | /// *`b_regular_faces_only`* – truncate (equiv. to `dkd`) 90 | /// * `v` – subdivide (Catmull-Clark) 91 | /// * `w` *`uf_ratio`*, *`f_height`* – whirl 92 | /// * `x` *`f_height`*, *`f_offset`*, *`[ui_face_arity_mask, …]`* – extrude 93 | /// * `z` *`f_height`*, *`[ui_vertex_valence_mask, …]`*, 94 | /// *`b_regular_faces_only`* – zip (equiv. to `dk`) 95 | /// 96 | /// # Examples 97 | /// 98 | /// ``` 99 | /// # use polyhedron_ops::Polyhedron; 100 | /// let polyhedron_from_str = 101 | /// Polyhedron::try_from("g0.2k0.1,[3,4],,{t}b,2T").unwrap(); 102 | /// 103 | /// let polyhedron_from_builder = Polyhedron::tetrahedron() 104 | /// .bevel(None, Some(2.0), None, None, true) 105 | /// .kis(Some(0.1), Some(&[3, 4]), None, Some(true), true) 106 | /// .gyro(Some(0.2), None, true) 107 | /// .finalize(); 108 | /// 109 | /// assert_eq!(polyhedron_from_str.name(), polyhedron_from_builder.name()); 110 | /// ``` 111 | fn try_from(s: &str) -> Result { 112 | let mut poly = Polyhedron::new(); 113 | 114 | let conway_notation_token_tree = ConwayPolyhedronNotationParser::parse( 115 | Rule::conway_notation_string, 116 | s, 117 | )?; 118 | 119 | // Reverse notation and skip end-of-input token now at the beginning 120 | // (EOI) 121 | conway_notation_token_tree.rev().skip(1).for_each(|pair| { 122 | let token = pair.clone().into_inner(); 123 | match pair.as_rule() { 124 | Rule::tetrahedron => { 125 | poly = Polyhedron::tetrahedron(); 126 | } 127 | Rule::hexahedron => { 128 | poly = Polyhedron::hexahedron(); 129 | } 130 | Rule::octahedron => { 131 | poly = Polyhedron::octahedron(); 132 | } 133 | Rule::dodecahedron => { 134 | poly = Polyhedron::dodecahedron(); 135 | } 136 | Rule::icosahedron => { 137 | poly = Polyhedron::icosahedron(); 138 | } 139 | Rule::prism => { 140 | poly = Polyhedron::prism(to_number(token).0); 141 | } 142 | Rule::antiprism => { 143 | poly = Polyhedron::antiprism(to_number(token).0); 144 | } 145 | Rule::ambo => { 146 | poly.ambo(to_number(token).0, true); 147 | } 148 | Rule::bevel => { 149 | let (ratio, token) = to_number(token); 150 | let (height, token) = to_number(token); 151 | let (vertex_valence, token) = to_vec(token); 152 | let (regular, _) = to_bool(token); 153 | poly.bevel( 154 | ratio, 155 | height, 156 | if vertex_valence.is_empty() { 157 | None 158 | } else { 159 | Some(vertex_valence.as_slice()) 160 | }, 161 | regular, 162 | true, 163 | ); 164 | } 165 | Rule::catmull_clark_subdivide => { 166 | poly.catmull_clark_subdivide(true); 167 | } 168 | Rule::chamfer => { 169 | poly.chamfer(to_number(token).0, true); 170 | } 171 | Rule::dual => { 172 | poly.dual(true); 173 | } 174 | Rule::expand => { 175 | poly.expand(to_number(token).0, true); 176 | } 177 | Rule::extrude => { 178 | let (height, token) = to_number(token); 179 | let (offset, token) = to_number(token); 180 | let (face_arity_mask, _) = to_vec(token); 181 | poly.extrude( 182 | height, 183 | offset, 184 | if face_arity_mask.is_empty() { 185 | None 186 | } else { 187 | Some(face_arity_mask.as_slice()) 188 | }, 189 | true, 190 | ); 191 | } 192 | Rule::gyro => { 193 | let (ratio, token) = to_number(token); 194 | let (height, _) = to_number(token); 195 | poly.gyro(ratio, height, true); 196 | } 197 | Rule::inset => { 198 | let (offset, token) = to_number(token); 199 | let (face_arity_mask, _) = to_vec(token); 200 | poly.inset( 201 | offset, 202 | if face_arity_mask.is_empty() { 203 | None 204 | } else { 205 | Some(face_arity_mask.as_slice()) 206 | }, 207 | true, 208 | ); 209 | } 210 | Rule::join => { 211 | poly.join(to_number(token).0, true); 212 | } 213 | Rule::kis => { 214 | let (height, token) = to_number(token); 215 | let (face_arity_mask, token) = to_vec(token); 216 | let (face_index_mask, token) = to_vec(token); 217 | let (regular, _) = to_bool(token); 218 | poly.kis( 219 | height, 220 | if face_arity_mask.is_empty() { 221 | None 222 | } else { 223 | Some(face_arity_mask.as_slice()) 224 | }, 225 | if face_index_mask.is_empty() { 226 | None 227 | } else { 228 | Some(face_index_mask.as_slice()) 229 | }, 230 | regular, 231 | true, 232 | ); 233 | } 234 | Rule::medial => { 235 | let (ratio, token) = to_number(token); 236 | let (height, token) = to_number(token); 237 | let (vertex_valence, token) = to_vec(token); 238 | let (regular, _) = to_bool(token); 239 | poly.medial( 240 | ratio, 241 | height, 242 | if vertex_valence.is_empty() { 243 | None 244 | } else { 245 | Some(vertex_valence.as_slice()) 246 | }, 247 | regular, 248 | true, 249 | ); 250 | } 251 | Rule::meta => { 252 | let (ratio, token) = to_number(token); 253 | let (height, token) = to_number(token); 254 | let (vertex_valence, token) = to_vec(token); 255 | let (regular, _) = to_bool(token); 256 | poly.meta( 257 | ratio, 258 | height, 259 | if vertex_valence.is_empty() { 260 | None 261 | } else { 262 | Some(vertex_valence.as_slice()) 263 | }, 264 | regular, 265 | true, 266 | ); 267 | } 268 | Rule::needle => { 269 | let (height, token) = to_number(token); 270 | let (vertex_valence, token) = to_vec(token); 271 | let (regular, _) = to_bool(token); 272 | poly.needle( 273 | height, 274 | if vertex_valence.is_empty() { 275 | None 276 | } else { 277 | Some(vertex_valence.as_slice()) 278 | }, 279 | regular, 280 | true, 281 | ); 282 | } 283 | Rule::ortho => { 284 | poly.ortho(to_number(token).0, true); 285 | } 286 | Rule::planarize => { 287 | poly.planarize(to_number(token).0, true); 288 | } 289 | Rule::propellor => { 290 | poly.propellor(to_number(token).0, true); 291 | } 292 | Rule::quinto => { 293 | poly.quinto(to_number(token).0, true); 294 | } 295 | Rule::reflect => { 296 | poly.reflect(true); 297 | } 298 | Rule::snub => { 299 | let (ratio, token) = to_number(token); 300 | let (height, _) = to_number(token); 301 | poly.snub(ratio, height, true); 302 | } 303 | Rule::spherize => { 304 | poly.spherize(to_number(token).0, true); 305 | } 306 | Rule::truncate => { 307 | let (height, token) = to_number(token); 308 | let (vertex_valence_mask, token) = to_vec(token); 309 | let (regular, _) = to_bool(token); 310 | poly.truncate( 311 | height, 312 | if vertex_valence_mask.is_empty() { 313 | None 314 | } else { 315 | Some(vertex_valence_mask.as_slice()) 316 | }, 317 | regular, 318 | true, 319 | ); 320 | } 321 | Rule::whirl => { 322 | let (ratio, token) = to_number(token); 323 | let (height, _) = to_number(token); 324 | poly.whirl(ratio, height, true); 325 | } 326 | Rule::zip => { 327 | let (height, token) = to_number(token); 328 | let (vertex_valence_mask, token) = to_vec(token); 329 | let (regular, _) = to_bool(token); 330 | poly.zip( 331 | height, 332 | if vertex_valence_mask.is_empty() { 333 | None 334 | } else { 335 | Some(vertex_valence_mask.as_slice()) 336 | }, 337 | regular, 338 | true, 339 | ); 340 | } 341 | _ => (), 342 | } 343 | poly.normalize(); 344 | }); 345 | 346 | Ok(poly) 347 | } 348 | } 349 | 350 | fn is_empty_or_comma(mut tokens: Pairs<'_, Rule>) -> (bool, Pairs<'_, Rule>) { 351 | // No more tokens? Return None. 352 | match tokens.clone().next() { 353 | Some(token) => { 354 | if Rule::separator == token.as_rule() { 355 | tokens.next(); 356 | (true, tokens) 357 | } else { 358 | (false, tokens) 359 | } 360 | } 361 | None => (true, tokens), 362 | } 363 | } 364 | 365 | fn to_bool(tokens: Pairs<'_, Rule>) -> (Option, Pairs<'_, Rule>) { 366 | let (exit, mut tokens) = is_empty_or_comma(tokens); 367 | 368 | if exit { 369 | return (None, tokens); 370 | } 371 | 372 | let result = match tokens.next().unwrap().as_str() { 373 | "{t}" => Some(true), 374 | "{f}" => Some(false), 375 | _ => None, 376 | }; 377 | 378 | (result, tokens) 379 | } 380 | 381 | fn to_number(tokens: Pairs<'_, Rule>) -> (Option, Pairs<'_, Rule>) 382 | where 383 | T: FromStr, 384 | ::Err: Debug, 385 | { 386 | let (exit, mut tokens) = is_empty_or_comma(tokens); 387 | 388 | if exit { 389 | return (None, tokens); 390 | } 391 | 392 | // Parse the next token as a number. 393 | let value = tokens.next().unwrap().as_str().parse::().unwrap(); 394 | 395 | // Skip possible trailing seprarator. 396 | tokens.next(); 397 | 398 | (Some(value), tokens) 399 | } 400 | 401 | fn to_vec(tokens: Pairs<'_, Rule>) -> (Vec, Pairs<'_, Rule>) 402 | where 403 | T: FromStr, 404 | ::Err: Debug, 405 | { 406 | let (exit, mut tokens) = is_empty_or_comma(tokens); 407 | 408 | if exit { 409 | return (Vec::new(), tokens); 410 | } 411 | 412 | let vertex_valence = tokens 413 | .clone() 414 | .take_while(|token| Rule::separator != token.as_rule()) 415 | .map(|token| token.as_str().parse::().unwrap()) 416 | .collect::>(); 417 | 418 | if !vertex_valence.is_empty() { 419 | tokens.next(); 420 | tokens.next(); 421 | } 422 | 423 | // Skip trailing separator. 424 | tokens.next(); 425 | 426 | (vertex_valence, tokens) 427 | } 428 | -------------------------------------------------------------------------------- /src/selection.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Selection methods. 4 | impl Polyhedron { 5 | /// Selects all faces within a half space defined by a plane through 6 | /// `origin` and the the give plane `normal`. 7 | pub fn select_faces_above_plane( 8 | &self, 9 | origin: Point, 10 | normal: Vector, 11 | ) -> Vec { 12 | self.face_index 13 | .iter() 14 | .enumerate() 15 | .filter_map(|(face_number, face)| { 16 | if face.iter().all(|&vertex_key| { 17 | is_point_inside_half_space( 18 | self.positions[vertex_key as usize], 19 | origin, 20 | normal, 21 | ) 22 | }) { 23 | Some(face_number as _) 24 | } else { 25 | None 26 | } 27 | }) 28 | .collect() 29 | } 30 | } 31 | 32 | pub(crate) fn is_face_selected( 33 | face: &Face, 34 | index: usize, 35 | positions: &Points, 36 | face_arity_mask: Option<&[usize]>, 37 | face_index_mask: Option<&[FaceKey]>, 38 | regular_faces_only: Option, 39 | ) -> bool { 40 | face_arity_mask.map_or_else( 41 | || true, 42 | |face_arity_mask| face_arity_mask.contains(&face.len()), 43 | ) && face_index_mask.map_or_else( 44 | || true, 45 | |face_index_mask| face_index_mask.contains(&(index as _)), 46 | ) && (!regular_faces_only.unwrap_or(false) 47 | || ((face_irregularity(face, positions) - 1.0).abs() < 0.1)) 48 | } 49 | 50 | #[inline] 51 | fn is_point_inside_half_space( 52 | point: Point, 53 | plane_origin: Point, 54 | plane_normal: Vector, 55 | ) -> bool { 56 | let point_to_plane = point - plane_origin; 57 | let distance = point_to_plane.dot(plane_normal); 58 | distance > 0.0 59 | } 60 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[test] 4 | fn tetrahedron_to_terahedron() { 5 | // Tetrahedron 6 | 7 | let mut tetrahedron = Polyhedron::tetrahedron(); 8 | 9 | //tetrahedron.dual(); 10 | tetrahedron.kis(Some(0.3), None, None, None, false); 11 | 12 | #[cfg(feature = "obj")] 13 | tetrahedron 14 | .write_obj(&std::path::PathBuf::from("."), false) 15 | .unwrap(); 16 | } 17 | 18 | #[test] 19 | fn cube_to_octahedron() { 20 | let mut cube = Polyhedron::hexahedron(); 21 | 22 | cube.dual(false); 23 | #[cfg(feature = "obj")] 24 | cube.write_obj(&std::path::PathBuf::from("."), false) 25 | .unwrap(); 26 | } 27 | 28 | #[test] 29 | fn triangulate_cube() { 30 | let mut cube = Polyhedron::hexahedron(); 31 | 32 | cube.triangulate(Some(true)); 33 | #[cfg(feature = "obj")] 34 | cube.write_obj(&std::path::PathBuf::from("."), false) 35 | .unwrap(); 36 | } 37 | 38 | #[test] 39 | fn make_prisms() { 40 | for i in 3..9 { 41 | let prism = Polyhedron::prism(Some(i)); 42 | 43 | #[cfg(feature = "obj")] 44 | prism 45 | .write_obj(&std::path::PathBuf::from("."), false) 46 | .unwrap(); 47 | 48 | let f = prism.faces().len(); 49 | let v = prism.positions_len(); 50 | let e = prism.to_edges().len(); 51 | assert!(f == i + 2); 52 | assert!(v == i * 2); 53 | assert!(e == 2 * i + i); 54 | assert!(f + v - e == 2); // Euler's Formula 55 | } 56 | } 57 | 58 | #[test] 59 | fn make_antiprisms() { 60 | for i in 3..9 { 61 | let antiprism = Polyhedron::antiprism(Some(i)); 62 | 63 | #[cfg(feature = "obj")] 64 | antiprism 65 | .write_obj(&std::path::PathBuf::from("."), false) 66 | .unwrap(); 67 | 68 | let f = antiprism.faces().len(); 69 | let v = antiprism.positions_len(); 70 | let e = antiprism.to_edges().len(); 71 | assert!(f == i * 2 + 2); 72 | assert!(v == i * 2); 73 | assert!(e == 2 * i + 2 * i); 74 | assert!(f + v - e == 2); // Euler's Formula 75 | } 76 | } 77 | 78 | #[cfg(feature = "parser")] 79 | mod parser_tests { 80 | use crate::*; 81 | 82 | #[test] 83 | fn test_ambo() { 84 | let poly_from_str = Polyhedron::try_from("a0.2T").unwrap(); 85 | let poly_from_ops = 86 | Polyhedron::tetrahedron().ambo(Some(0.2), true).finalize(); 87 | 88 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 89 | } 90 | 91 | // All tests for the remaining operators 92 | #[test] 93 | fn test_bevel() { 94 | let poly_from_str = Polyhedron::try_from("b0.2,,,{t}T").unwrap(); 95 | let poly_from_ops = Polyhedron::tetrahedron() 96 | .bevel(Some(0.2), None, None, Some(true), true) 97 | .finalize(); 98 | 99 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 100 | } 101 | 102 | #[test] 103 | fn test_catmull_clark_subdivide() { 104 | let poly_from_str = Polyhedron::try_from("vT").unwrap(); 105 | let poly_from_ops = Polyhedron::tetrahedron() 106 | .catmull_clark_subdivide(true) 107 | .finalize(); 108 | 109 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 110 | } 111 | 112 | #[test] 113 | fn test_chamfer() { 114 | let poly_from_str = Polyhedron::try_from("c0.2T").unwrap(); 115 | let poly_from_ops = Polyhedron::tetrahedron() 116 | .chamfer(Some(0.2), true) 117 | .finalize(); 118 | 119 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 120 | } 121 | 122 | #[test] 123 | fn test_dual() { 124 | let poly_from_str = Polyhedron::try_from("dT").unwrap(); 125 | let poly_from_ops = Polyhedron::tetrahedron().dual(true).finalize(); 126 | 127 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 128 | } 129 | 130 | #[test] 131 | fn test_extrude() { 132 | let poly_from_str = Polyhedron::try_from("x0.2T").unwrap(); 133 | let poly_from_ops = Polyhedron::tetrahedron() 134 | .extrude(Some(0.2), None, None, true) 135 | .finalize(); 136 | 137 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 138 | } 139 | 140 | #[test] 141 | fn test_gyro() { 142 | let poly_from_str = Polyhedron::try_from("g0.2T").unwrap(); 143 | let poly_from_ops = Polyhedron::tetrahedron() 144 | .gyro(Some(0.2), None, true) 145 | .finalize(); 146 | 147 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 148 | } 149 | 150 | #[test] 151 | fn test_inset() { 152 | let poly_from_str = Polyhedron::try_from("i0.2T").unwrap(); 153 | let poly_from_ops = Polyhedron::tetrahedron() 154 | .inset(Some(0.2), None, true) 155 | .finalize(); 156 | 157 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 158 | } 159 | 160 | #[test] 161 | fn test_kis() { 162 | let poly_from_str = Polyhedron::try_from("k0.2,,,{t}T").unwrap(); 163 | let poly_from_ops = Polyhedron::tetrahedron() 164 | .kis(Some(0.2), None, None, Some(true), true) 165 | .finalize(); 166 | 167 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 168 | } 169 | 170 | #[test] 171 | fn test_medial() { 172 | let poly_from_str = Polyhedron::try_from("M,0.3T").unwrap(); 173 | let poly_from_ops = Polyhedron::tetrahedron() 174 | .medial(None, Some(0.3), None, None, true) 175 | .finalize(); 176 | 177 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 178 | } 179 | 180 | #[test] 181 | fn test_meta() { 182 | let poly_from_str = Polyhedron::try_from("m0.2,,,{t}T").unwrap(); 183 | let poly_from_ops = Polyhedron::tetrahedron() 184 | .meta(Some(0.2), None, None, Some(true), true) 185 | .finalize(); 186 | 187 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 188 | } 189 | 190 | #[test] 191 | fn test_needle() { 192 | let poly_from_str = Polyhedron::try_from("n0.01T").unwrap(); 193 | let poly_from_ops = Polyhedron::tetrahedron() 194 | .needle(Some(0.01), None, None, true) 195 | .finalize(); 196 | 197 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 198 | } 199 | 200 | #[test] 201 | fn test_ortho() { 202 | let poly_from_str = Polyhedron::try_from("o0.8T").unwrap(); 203 | let poly_from_ops = 204 | Polyhedron::tetrahedron().ortho(Some(0.8), true).finalize(); 205 | 206 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 207 | } 208 | 209 | #[test] 210 | fn test_propellor() { 211 | let poly_from_str = Polyhedron::try_from("p0.2T").unwrap(); 212 | let poly_from_ops = Polyhedron::tetrahedron() 213 | .propellor(Some(0.2), true) 214 | .finalize(); 215 | 216 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 217 | } 218 | 219 | #[test] 220 | fn test_quinto() { 221 | let poly_from_str = Polyhedron::try_from("q0.2T").unwrap(); 222 | let poly_from_ops = 223 | Polyhedron::tetrahedron().quinto(Some(0.2), true).finalize(); 224 | 225 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 226 | } 227 | 228 | #[test] 229 | fn test_reflect() { 230 | let poly_from_str = Polyhedron::try_from("rT").unwrap(); 231 | let poly_from_ops = Polyhedron::tetrahedron().reflect(true).finalize(); 232 | 233 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 234 | } 235 | 236 | #[test] 237 | fn test_spherize() { 238 | let poly_from_str = Polyhedron::try_from("S0.9T").unwrap(); 239 | let poly_from_ops = Polyhedron::tetrahedron() 240 | .spherize(Some(0.9), true) 241 | .finalize(); 242 | 243 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 244 | } 245 | 246 | #[test] 247 | fn test_snub() { 248 | let poly_from_str = Polyhedron::try_from("s,0.3T").unwrap(); 249 | let poly_from_ops = Polyhedron::tetrahedron() 250 | .snub(None, Some(0.3), true) 251 | .finalize(); 252 | 253 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 254 | } 255 | 256 | #[test] 257 | fn test_truncate() { 258 | let poly_from_str = Polyhedron::try_from("t0.2T").unwrap(); 259 | let poly_from_ops = Polyhedron::tetrahedron() 260 | .truncate(Some(0.2), None, None, true) 261 | .finalize(); 262 | 263 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 264 | } 265 | 266 | #[test] 267 | fn test_whirl() { 268 | let poly_from_str = Polyhedron::try_from("w0.2T").unwrap(); 269 | let poly_from_ops = Polyhedron::tetrahedron() 270 | .whirl(Some(0.2), None, true) 271 | .finalize(); 272 | 273 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 274 | } 275 | 276 | #[test] 277 | fn test_zip() { 278 | let poly_from_str = Polyhedron::try_from("z0.2T").unwrap(); 279 | let poly_from_ops = Polyhedron::tetrahedron() 280 | .zip(Some(0.2), None, None, true) 281 | .finalize(); 282 | 283 | assert_eq!(poly_from_str.name(), poly_from_ops.name()); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/text_helpers.rs: -------------------------------------------------------------------------------- 1 | use num_traits::{Float, FromPrimitive}; 2 | use std::fmt::{Display, Write}; 3 | 4 | #[inline] 5 | pub(crate) fn format_float(x: T) -> String 6 | where 7 | T: Float + FromPrimitive + Display, 8 | { 9 | let int_part = x.trunc(); 10 | let decimal_part = x - int_part; 11 | 12 | if decimal_part.abs() < T::epsilon() { 13 | format!("{}", int_part.to_i64().unwrap()) 14 | } else { 15 | let decimal_digits = (-x.abs().log10().floor()).to_usize().unwrap(); 16 | format!("{:.*}", decimal_digits, x) 17 | } 18 | } 19 | 20 | #[test] 21 | fn test_format_float() { 22 | assert_eq!(format_float(1.0), "1"); 23 | assert_eq!(format_float(-0.2), "-0.2"); 24 | assert_eq!(format_float(0.30), "0.3"); 25 | } 26 | 27 | pub(crate) fn _format_float_slice(slice: &[T]) -> String 28 | where 29 | T: Float + FromPrimitive + Display, 30 | { 31 | if slice.is_empty() { 32 | String::new() 33 | } else { 34 | let mut string = String::with_capacity(slice.len() * 2); 35 | if 1 == slice.len() { 36 | write!(&mut string, "{}", format_float(slice[0])).unwrap(); 37 | } else { 38 | string.push('['); 39 | write!(&mut string, "{}", format_float(slice[0])).unwrap(); 40 | for i in slice.get(1..).unwrap() { 41 | write!(&mut string, ",{}", format_float(*i)).unwrap(); 42 | } 43 | string.push(']'); 44 | } 45 | string 46 | } 47 | } 48 | 49 | pub(crate) fn format_integer_slice(slice: &[T]) -> String 50 | where 51 | T: Display, 52 | { 53 | if slice.is_empty() { 54 | String::new() 55 | } else { 56 | let mut string = String::with_capacity(slice.len() * 2); 57 | if 1 == slice.len() { 58 | write!(&mut string, "{}", slice[0]).unwrap(); 59 | } else { 60 | string.push('['); 61 | write!(&mut string, "{}", slice[0]).unwrap(); 62 | for i in slice.get(1..).unwrap() { 63 | write!(&mut string, ",{}", i).unwrap(); 64 | } 65 | string.push(']'); 66 | } 67 | string 68 | } 69 | } 70 | --------------------------------------------------------------------------------