├── .clang-format ├── .github └── workflows │ ├── clangfmt.yml │ ├── clippy.yml │ ├── rustfmt.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── kicad-parser │ ├── Cargo.toml │ ├── sample-files │ │ └── sample.kicad_pcb │ └── src │ │ ├── board.rs │ │ ├── graphics.rs │ │ └── lib.rs ├── model-api │ ├── Cargo.toml │ ├── src │ │ ├── angle.rs │ │ ├── lib.rs │ │ ├── primitives.rs │ │ ├── primitives │ │ │ ├── compound.rs │ │ │ ├── edge.rs │ │ │ ├── face.rs │ │ │ ├── shape.rs │ │ │ ├── shell.rs │ │ │ ├── solid.rs │ │ │ └── wire.rs │ │ ├── wasm.rs │ │ └── workplane.rs │ └── wit │ │ └── model-api.wit ├── occt-sys │ ├── Cargo.toml │ ├── build.rs │ ├── examples │ │ └── print_paths.rs │ ├── patch │ │ └── adm │ │ │ ├── MODULES │ │ │ └── RESOURCES │ └── src │ │ └── lib.rs ├── opencascade-sys │ ├── Cargo.toml │ ├── OCCT │ │ └── CMakeLists.txt │ ├── build.rs │ ├── examples │ │ ├── bottle.rs │ │ └── simple.rs │ ├── include │ │ └── wrapper.hxx │ ├── src │ │ └── lib.rs │ └── tests │ │ └── triangulation.rs ├── opencascade │ ├── Cargo.toml │ └── src │ │ ├── angle.rs │ │ ├── kicad.rs │ │ ├── law_function.rs │ │ ├── lib.rs │ │ ├── make_pipe_shell.rs │ │ ├── mesh.rs │ │ ├── primitives.rs │ │ ├── primitives │ │ ├── boolean_shape.rs │ │ ├── compound.rs │ │ ├── edge.rs │ │ ├── face.rs │ │ ├── shape.rs │ │ ├── shell.rs │ │ ├── solid.rs │ │ ├── surface.rs │ │ ├── vertex.rs │ │ └── wire.rs │ │ ├── section.rs │ │ └── workplane.rs ├── viewer │ ├── Cargo.toml │ ├── build.rs │ ├── models │ │ └── nist_ftc_06.step │ ├── shaders │ │ ├── edge.wgsl │ │ └── surface.wgsl │ └── src │ │ ├── camera.rs │ │ ├── edge_drawer.rs │ │ ├── main.rs │ │ ├── surface_drawer.rs │ │ └── wasm_engine.rs └── wasm-example │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── docs ├── images │ └── cascade_library.png └── writing_bindings.md ├── examples ├── Cargo.toml └── src │ ├── airfoil.rs │ ├── box_shape.rs │ ├── cable_bracket.rs │ ├── chamfer.rs │ ├── flat_ethernet_bracket.rs │ ├── gizmo.rs │ ├── heater_coil.rs │ ├── high_level_bottle.rs │ ├── keyboard_case.rs │ ├── keycap.rs │ ├── letter_a.rs │ ├── lib.rs │ ├── offset_2d.rs │ ├── rounded_chamfer.rs │ ├── section.rs │ ├── swept_face.rs │ ├── swept_face_variable.rs │ ├── swept_wire.rs │ ├── swept_wire_variable.rs │ ├── turners_cube.rs │ ├── variable_fillet.rs │ ├── write_model.rs │ └── zbox_case.rs └── rustfmt.toml /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | ColumnLimit: 120 5 | -------------------------------------------------------------------------------- /.github/workflows/clangfmt.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Clang Format 8 | 9 | jobs: 10 | fmt: 11 | name: Clang Format 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: jidicula/clang-format-action@v4.9.0 16 | with: 17 | clang-format-version: '14' 18 | check-path: crates/opencascade-sys/include 19 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Cargo Clippy 8 | 9 | jobs: 10 | clippy: 11 | name: Cargo Clippy 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: "recursive" 17 | - uses: dtolnay/rust-toolchain@v1 18 | with: 19 | toolchain: stable 20 | components: clippy 21 | - name: Cache Rust Dependencies 22 | uses: Swatinem/rust-cache@v2 23 | - run: cargo clippy --all-targets -- -D warnings 24 | -------------------------------------------------------------------------------- /.github/workflows/rustfmt.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Cargo Format 8 | 9 | jobs: 10 | fmt: 11 | name: Cargo Format 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: dtolnay/rust-toolchain@v1 16 | with: 17 | toolchain: nightly 18 | components: rustfmt 19 | - run: cargo fmt --all -- --check 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: Cargo Test 8 | 9 | jobs: 10 | test: 11 | name: Cargo Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: "recursive" 17 | - uses: dtolnay/rust-toolchain@v1 18 | with: 19 | toolchain: stable 20 | - name: Cache Rust Dependencies 21 | uses: Swatinem/rust-cache@v2 22 | - run: cargo test --all-targets 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | /Cargo.lock 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "crates/opencascade-sys/OCCT"] 2 | path = crates/occt-sys/OCCT 3 | url = https://github.com/Open-Cascade-SAS/OCCT.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/kicad-parser", "crates/model-api", 4 | "crates/opencascade", 5 | "crates/opencascade-sys", 6 | "crates/viewer", "crates/wasm-example", 7 | "examples", 8 | ] 9 | 10 | resolver = "2" 11 | 12 | # Cargo by default builds build (only) dependencies with opt-level of 0 even in the release profile. 13 | # That makes sense, as such code is normally run only once. But `occt-sys` is special: it is a build 14 | # dependency of `opencascade-sys`, but it compiles static libraries that do end up in the final 15 | # binaries. 16 | # So set the regular release opt-level. `cmake` crate then picks it up and passes to the C++ build. 17 | [profile.release.package.occt-sys] 18 | opt-level = 3 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opencascade-rs 2 | 3 | Rust bindings to [OpenCascade](https://dev.opencascade.org). The code is currently a major work in progress. 4 | 5 | I currently work a full-time job and work on this in my spare time, so please adjust timing expectations accordingly :) 6 | 7 | ## Major Goals 8 | * Define 3D CAD models suitable for 3D printing or machining, in ergonomic Rust code 9 | * Code-first approach, but allow use of a GUI where it makes sense (2D sketches) 10 | * Support fillets, chamfers, lofts, surface filling, pipes, extrusions, revolutions, etc. 11 | * Support quick compile times for faster iterations 12 | * Ability to import/export STEP files, STL files, SVG, DXF, KiCAD files, and hopefully more! 13 | * Easy to install the viewer app (provide pre-built binaries for major platforms) 14 | * Easy to create and use user-authored libraries (via crates.io) for quick and easy code-sharing 15 | * Pretty visualizations of created parts 16 | * Ability to specify assemblies of parts, and constraints between assembled parts 17 | 18 | ## Rationale 19 | 20 | This project was born out of me designing [my own keyboard](https://github.com/bschwind/key-ripper) and wanting to make a 3D-printed or CNCed aluminum case for it. In typical over-engineering fashion, I didn't want to just use Fusion360 and call it a day. I wanted a fully parameterized, fully open-source, code-based approach so I can easily make changes, and store the model code in version control. I also want to be fairly confident I can build these models any time in the future given I have a C++/Rust toolchain available. 21 | 22 | So I researched what kernels are out there, learned that OpenCascade is one of the few open-source B-Rep (boundary representation) kernels available, and started writing bindings to it with cxx.rs to see if usage of the kernel is feasible. Turns out it is! 23 | 24 | ### Why Rust? 25 | 26 | At this point I'm most comfortable with Rust, so most tools I build will be with Rust. I also don't find any joy in creating my own language or forcing people to learn one I created. Rust is a far better language than I could ever make myself, and contains pretty much every facility I would want for defining 3D models in code. Ultimately it's a hobby project and when you run a hobby project, you get to pick whatever you want :) 27 | 28 | There are other benefits: 29 | 30 | * Easy to install the Rust toolchain 31 | * Strong type system can inform you what you can do with say, a `Wire` or a `Shape` 32 | * Great generated documentation 33 | * Good cross-platform support 34 | * Excellent library ecosystem on crates.io, making parts sharing a breeze 35 | * High level Rust can be ergonomic, with iterators, closures, operator overloading, and enums 36 | * Rust's unique (`&mut T`) and shared (`&T`) references and function type signatures inform you when an operation modifies a shape vs. creating a new one 37 | 38 | ## Dependencies 39 | 40 | * Rust Toolchain (https://rustup.rs/) 41 | * CMake (https://cmake.org/) 42 | * A C++ compiler with C++11 support 43 | 44 | ## Building 45 | 46 | * The `OCCT` codebase is included as a git submodule. Clone the repo with the `--recursive` flag, or use `git submodule update --init` to fetch the submodule. 47 | * `cargo build --release` 48 | 49 | ### Using pre-installed OpenCASCADE 50 | 51 | If you have the `OCCT` library already installed via a package manager, you can dynamically link to it which will significantly decrease build times. By default, the `builtin` feature is enabled which means compiling OCCT from source. You can disable it via the command line: 52 | 53 | `cargo build --no-default-features` 54 | 55 | or by specifying `default-features = false` in your `Cargo.toml`: 56 | 57 | ``` 58 | [dependencies] 59 | opencascade = { version = "0.2", default-features = false } 60 | ``` 61 | 62 | NOTE: If you have installed `OCCT` manually you may need specify the path to it via the `DEP_OCCT_ROOT` environment variable. The specified root directory usually contains `include` and `lib` directories. 63 | 64 | ## Run Examples 65 | 66 | * `cargo run --release --example bottle` 67 | 68 | The program will output `bottle.stl` in the current working directory. 69 | 70 | ### Lower Level 71 | 72 | There are low level examples which are more or less directly calling OpenCascade functions, such as the classic OpenCascade [bottle](./crates/opencascade-sys/examples/bottle.rs) example, or a [simpler](./crates/opencascade-sys/examples/simple.rs) one. 73 | 74 | ### Higher Level 75 | 76 | The [higher level examples](./crates/opencascade/examples) use more ergonomic Rust APIs, though the exact API is still in flux and subject to change. 77 | 78 | ## Viewer Application 79 | 80 | There is currently an experimental viewer application based on WGPU, which will probably become the "main" way people use this crate. It currently visualizes one of the examples, but will expand to be capable of loading Rust model code compiled to WASM, allowing faster compile times and more interactive inspection of the sketches and models. 81 | 82 | To e.g. visualize the keycap example, you can run the current viewer app with 83 | 84 | ``` 85 | $ cargo run --release --bin viewer -- --example keycap 86 | ``` 87 | 88 | To view a STEP file: 89 | 90 | ``` 91 | $ cargo run --release --bin viewer -- --step-file SOME_FILE.step 92 | ``` 93 | 94 | ## Example Model Writer 95 | 96 | You can write an example model to a file using the `write_model` binary in `examples`. 97 | 98 | For more information, run the following command: 99 | 100 | ``` 101 | cargo run --bin write_model -- --help 102 | ``` 103 | 104 | ## Code Formatting 105 | 106 | ### Rust Code 107 | ``` 108 | $ cargo +nightly fmt 109 | ``` 110 | 111 | ### C++ Code 112 | ``` 113 | $ clang-format -i crates/opencascade-sys/include/wrapper.hxx 114 | ``` 115 | 116 | ## Comparison to other tools 117 | 118 | ### OpenCascade C++ API 119 | 120 | This is probably an obvious one, but I use Rust in order to avoid using C++ when possible. You can use OpenCascade directly in its native language, C++, and some people do! I don't have the patience or mental fortitude for it, though. This method of course gives you the full power of OpenCascade without having to write bindings or higher-level wrappers for it. 121 | 122 | ### [OpenSCAD](https://openscad.org/) 123 | 124 | OpenSCAD is how I started with code-based CAD, and it's still a nice tool with lots of community projects and libraries invested into it. To me though, there are several downsides: 125 | 126 | * The language is clumsy and limited compared to modern programming languages 127 | * The CAD kernel is CGAL, which is mesh-based. There is less semantic information about geometry, and parts end up just being a soup of triangles. 128 | * Fillets, chamfers, and curves in general end up being more of a pain compared to a B-Rep (Boundary Representation) CAD kernel 129 | * No ability to export STEP files 130 | 131 | 132 | ### [CadQuery](https://cadquery.readthedocs.io/en/latest/) 133 | 134 | This project is extremely similar to CadQuery, and owes a lot of its inspiration to it. I mostly like CadQuery, except: 135 | 136 | * It's a Python tool, and managing Python dependencies and installations just isn't fun 137 | * The usage of the "fluent" API produces code that is hard to visualize, you have to keep a lot on your mental stack to understand what a given snippet is doing. 138 | 139 | These are small complaints, and to the second point, I'm pretty sure you can write more imperative CadQuery code which spells out more obviously what is going on. 140 | 141 | I'd say CadQuery is an _excellent_ tool, and likely the most fully-featured code-based CAD tool out there that I'm aware of. 142 | 143 | So if you like Python and have patience to deal with Python installations and such, absolutely go with CadQuery. It'll take this project quite awhile to reach feature parity with it. 144 | 145 | ### [Build123d](https://github.com/gumyr/build123d) 146 | 147 | Build123d seems to be an evolution of CadQuery. Still in Python, it replaces the "fluent" API with stateful context managers using `with` blocks. It's still an early project and I haven't looked closely at it, but I do wonder if the context-manager approach will lead to lots of rightward drift in code. Aside from that, it seems like a reasonable syntax approach for CAD modeling. 148 | 149 | Still has the same downsides of managing a Python installation and managing how you distribute that. 150 | 151 | ### [Cascade Studio](https://github.com/zalo/CascadeStudio) 152 | 153 | Like CadQuery, Cascade Studio is also based on the OpenCascade kernel. It's quite nice as well, and has an [incredible manual](https://github.com/raydeleu/CascadeStudioManual) with tons of detail. I was mainly turned off by the fact that you have to use the GUI to discover edge indices, which you then pass to the `FilletEdges()` function as a list of numbers. These indices can change as you modify the shape, and it all feels a bit unstable and relies too much on mouse picking from the GUI. 154 | 155 | But its web browser support and relatively simple JavaScript API make it a nice, approachable tool if you want to create models quickly. 156 | 157 | ### [DeclaraCAD](https://declaracad.com/docs/introduction/) 158 | 159 | Also based on OpenCascade, DeclaraCAD aims to allow you to write a declarative tree which represents all the operations you perform to create a shape. It seems to have quite rich support for sketches, part modeling, and part assembly. It is distributed as a Qt application and is fully offline and driven by user text files - nice! I would personally worry about the rightward drift of code for non-trivial models, and my brain doesn't really think in a tree the way the code is structured, but if you're a LISPer this is probably perfect! 160 | 161 | ### [Fornjot](https://github.com/hannobraun/fornjot) 162 | 163 | Fornjot is an early-stage B-Rep kernel, written in Rust. I think the project has a lot of potential, but of course being an early-stage project, it's not nearly as featureful as something like OpenCascade, which has had decades of development behind it. 164 | 165 | At the same time, I think Rust gives you the power to take on large ambitious projects and keep things organized, so if the maintainer can keep momentum and build a community of contributors behind the project, we may have a nice, pure-Rust solution to code-based CAD. 166 | 167 | For now though, I'd rather build a nice Rust API on top of OpenCascade, and then perhaps add Fornjot as a backend to that API when the project is farther along. 168 | -------------------------------------------------------------------------------- /crates/kicad-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kicad-parser" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | thiserror = "1" 8 | sexp = "1.1.4" 9 | -------------------------------------------------------------------------------- /crates/kicad-parser/sample-files/sample.kicad_pcb: -------------------------------------------------------------------------------- 1 | (kicad_pcb (version 20221018) (generator pcbnew) 2 | 3 | (general 4 | (thickness 0.89) 5 | ) 6 | 7 | (paper "A4") 8 | (layers 9 | (0 "F.Cu" signal) 10 | (31 "B.Cu" signal) 11 | (32 "B.Adhes" user "B.Adhesive") 12 | (33 "F.Adhes" user "F.Adhesive") 13 | (34 "B.Paste" user) 14 | (35 "F.Paste" user) 15 | (36 "B.SilkS" user "B.Silkscreen") 16 | (37 "F.SilkS" user "F.Silkscreen") 17 | (38 "B.Mask" user) 18 | (39 "F.Mask" user) 19 | (40 "Dwgs.User" user "User.Drawings") 20 | (41 "Cmts.User" user "User.Comments") 21 | (42 "Eco1.User" user "User.Eco1") 22 | (43 "Eco2.User" user "User.Eco2") 23 | (44 "Edge.Cuts" user) 24 | (45 "Margin" user) 25 | (46 "B.CrtYd" user "B.Courtyard") 26 | (47 "F.CrtYd" user "F.Courtyard") 27 | (48 "B.Fab" user) 28 | (49 "F.Fab" user) 29 | (50 "User.1" user) 30 | (51 "User.2" user) 31 | (52 "User.3" user) 32 | (53 "User.4" user) 33 | (54 "User.5" user) 34 | (55 "User.6" user) 35 | (56 "User.7" user) 36 | (57 "User.8" user) 37 | (58 "User.9" user) 38 | ) 39 | 40 | (setup 41 | (pad_to_mask_clearance 0) 42 | (pcbplotparams 43 | (layerselection 0x00010fc_ffffffff) 44 | (plot_on_all_layers_selection 0x0000000_00000000) 45 | (disableapertmacros false) 46 | (usegerberextensions false) 47 | (usegerberattributes true) 48 | (usegerberadvancedattributes true) 49 | (creategerberjobfile true) 50 | (dashed_line_dash_ratio 12.000000) 51 | (dashed_line_gap_ratio 3.000000) 52 | (svgprecision 4) 53 | (plotframeref false) 54 | (viasonmask false) 55 | (mode 1) 56 | (useauxorigin false) 57 | (hpglpennumber 1) 58 | (hpglpenspeed 20) 59 | (hpglpendiameter 15.000000) 60 | (dxfpolygonmode true) 61 | (dxfimperialunits true) 62 | (dxfusepcbnewfont true) 63 | (psnegative false) 64 | (psa4output false) 65 | (plotreference true) 66 | (plotvalue true) 67 | (plotinvisibletext false) 68 | (sketchpadsonfab false) 69 | (subtractmaskfromsilk false) 70 | (outputformat 1) 71 | (mirror false) 72 | (drillshape 1) 73 | (scaleselection 1) 74 | (outputdirectory "") 75 | ) 76 | ) 77 | 78 | (net 0 "") 79 | 80 | (gr_line (start 59 35) (end 59 43) 81 | (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 1a567b5b-971c-4afb-a619-c401fcf8a44e)) 82 | (gr_line (start 59 43) (end 43 43) 83 | (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 5056c2d9-f717-42bb-8113-318d910b504d)) 84 | (gr_line (start 33 33) (end 33 25) 85 | (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 68d1f2f3-85fe-4625-b285-bc3431b96d8f)) 86 | (gr_arc (start 49 25) (mid 56.071068 27.928932) (end 59 35) 87 | (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 697dd226-35eb-455d-a79a-10d11dd9dbd1)) 88 | (gr_arc (start 43 43) (mid 35.928932 40.071068) (end 33 33) 89 | (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 8cea329e-1099-4a38-8292-55c4bd39d46b)) 90 | (gr_line (start 33 25) (end 49 25) 91 | (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp aee5a981-e1d9-405d-a748-40680c7f708c)) 92 | 93 | ) 94 | -------------------------------------------------------------------------------- /crates/kicad-parser/src/board.rs: -------------------------------------------------------------------------------- 1 | use crate::{extract_number, Error}; 2 | use sexp::{Atom, Sexp}; 3 | use std::path::Path; 4 | 5 | use crate::graphics::{GraphicArc, GraphicCircle, GraphicLine, GraphicRect}; 6 | 7 | #[derive(Debug, Clone, Default)] 8 | pub struct KicadBoard { 9 | graphic_lines: Vec, 10 | graphic_arcs: Vec, 11 | graphic_circles: Vec, 12 | graphic_rects: Vec, 13 | footprints: Vec, 14 | } 15 | 16 | impl KicadBoard { 17 | pub fn from_file>(file: P) -> Result { 18 | let kicad_board_str = std::fs::read_to_string(&file)?; 19 | let sexp = sexp::parse(&kicad_board_str)?; 20 | 21 | let Sexp::List(list) = sexp else { 22 | return Err(Error::TopLevelObjectNotList); 23 | }; 24 | 25 | let Sexp::Atom(Atom::S(head)) = &list[0] else { 26 | return Err(Error::FirstElementInListNotString); 27 | }; 28 | 29 | match head.as_str() { 30 | "kicad_pcb" => { 31 | let board_fields = &list[1..]; 32 | Ok(Self::handle_board_fields(board_fields)?) 33 | }, 34 | _ => Err(Error::NotKicadPcbFile), 35 | } 36 | } 37 | 38 | pub fn footprints(&self) -> impl Iterator { 39 | self.footprints.iter() 40 | } 41 | 42 | pub fn lines(&self) -> impl Iterator { 43 | self.graphic_lines.iter() 44 | } 45 | 46 | pub fn arcs(&self) -> impl Iterator { 47 | self.graphic_arcs.iter() 48 | } 49 | 50 | pub fn circles(&self) -> impl Iterator { 51 | self.graphic_circles.iter() 52 | } 53 | 54 | pub fn rects(&self) -> impl Iterator { 55 | self.graphic_rects.iter() 56 | } 57 | 58 | fn handle_board_fields(fields: &[Sexp]) -> Result { 59 | let mut board = Self::default(); 60 | 61 | for field in fields { 62 | let Sexp::List(list) = field else { 63 | continue; 64 | }; 65 | 66 | let Sexp::Atom(Atom::S(head)) = &list[0] else { 67 | continue; 68 | }; 69 | 70 | let rest = &list[1..]; 71 | 72 | match head.as_str() { 73 | "version" => {}, 74 | "generator" => {}, 75 | "general" => {}, 76 | "paper" => {}, 77 | "layers" => {}, 78 | "footprint" => { 79 | let footprint = Footprint::from_list(rest)?; 80 | board.footprints.push(footprint); 81 | }, 82 | "gr_arc" => { 83 | let arc = GraphicArc::from_list(rest)?; 84 | board.graphic_arcs.push(arc); 85 | }, 86 | "gr_line" => { 87 | let line = GraphicLine::from_list(rest)?; 88 | board.graphic_lines.push(line); 89 | }, 90 | "gr_circle" => { 91 | let line = GraphicCircle::from_list(rest)?; 92 | board.graphic_circles.push(line); 93 | }, 94 | "gr_rect" => { 95 | let line = GraphicRect::from_list(rest)?; 96 | board.graphic_rects.push(line); 97 | }, 98 | _ => {}, 99 | } 100 | } 101 | 102 | Ok(board) 103 | } 104 | } 105 | 106 | #[derive(Debug, Clone, Default)] 107 | pub struct Footprint { 108 | pub location: (f64, f64), 109 | pub rotation_degrees: f64, 110 | graphic_lines: Vec, 111 | graphic_arcs: Vec, 112 | } 113 | 114 | impl Footprint { 115 | pub fn from_list(list: &[Sexp]) -> Result { 116 | let mut footprint = Self::default(); 117 | 118 | for field in list { 119 | let Sexp::List(list) = field else { 120 | continue; 121 | }; 122 | 123 | let Sexp::Atom(Atom::S(head)) = &list[0] else { 124 | continue; 125 | }; 126 | 127 | let rest = &list[1..]; 128 | 129 | match head.as_str() { 130 | "at" => match rest { 131 | [x, y] => { 132 | let x = extract_number(x)?; 133 | let y = extract_number(y)?; 134 | footprint.location = (x, y); 135 | }, 136 | [x, y, rotation_degrees] => { 137 | let x = extract_number(x)?; 138 | let y = extract_number(y)?; 139 | let rotation_degrees = extract_number(rotation_degrees)?; 140 | 141 | footprint.location = (x, y); 142 | footprint.rotation_degrees = rotation_degrees; 143 | }, 144 | _ => {}, 145 | }, 146 | "fp_arc" => { 147 | let arc = GraphicArc::from_list(rest)?; 148 | footprint.graphic_arcs.push(arc); 149 | }, 150 | "fp_line" => { 151 | let line = GraphicLine::from_list(rest)?; 152 | footprint.graphic_lines.push(line); 153 | }, 154 | _ => {}, 155 | } 156 | } 157 | 158 | Ok(footprint) 159 | } 160 | 161 | pub fn lines(&self) -> impl Iterator { 162 | // TODO - map from footprint space to world space 163 | self.graphic_lines.iter() 164 | } 165 | 166 | pub fn arcs(&self) -> impl Iterator { 167 | // TODO - map from footprint space to world space 168 | self.graphic_arcs.iter() 169 | } 170 | } 171 | 172 | #[derive(Debug, Clone, PartialEq)] 173 | pub enum BoardLayer { 174 | FCu, 175 | BCu, 176 | FAdhes, 177 | BAdhes, 178 | FPaste, 179 | BPaste, 180 | FSilkS, 181 | BSilkS, 182 | FMask, 183 | BFask, 184 | DwgsUser, 185 | CmtsUser, 186 | Eco1User, 187 | Eco2User, 188 | EdgeCuts, 189 | Margin, 190 | BCrtYd, 191 | FCrtYd, 192 | BFab, 193 | FFab, 194 | In1Cu, 195 | In2Cu, 196 | In3Cu, 197 | In4Cu, 198 | User(String), 199 | } 200 | 201 | impl From<&str> for BoardLayer { 202 | fn from(s: &str) -> Self { 203 | match s { 204 | "F.Cu" => BoardLayer::FCu, 205 | "B.Cu" => BoardLayer::BCu, 206 | "F.Adhes" => BoardLayer::FAdhes, 207 | "B.Adhes" => BoardLayer::BAdhes, 208 | "F.Paste" => BoardLayer::FPaste, 209 | "B.Paste" => BoardLayer::BPaste, 210 | "F.SilkS" => BoardLayer::FSilkS, 211 | "B.SilkS" => BoardLayer::BSilkS, 212 | "F.Mask" => BoardLayer::FMask, 213 | "B.Mask" => BoardLayer::BFask, 214 | "Dwgs.User" => BoardLayer::DwgsUser, 215 | "Cmts.User" => BoardLayer::CmtsUser, 216 | "Eco1.User" => BoardLayer::Eco1User, 217 | "Eco2.User" => BoardLayer::Eco2User, 218 | "Edge.Cuts" => BoardLayer::EdgeCuts, 219 | "Margin" => BoardLayer::Margin, 220 | "B.CrtYd" => BoardLayer::BCrtYd, 221 | "F.CrtYd" => BoardLayer::FCrtYd, 222 | "B.Fab" => BoardLayer::BFab, 223 | "F.Fab" => BoardLayer::FFab, 224 | "In1.Cu" => BoardLayer::In1Cu, 225 | "In2.Cu" => BoardLayer::In2Cu, 226 | "In3.Cu" => BoardLayer::In3Cu, 227 | "In4.Cu" => BoardLayer::In4Cu, 228 | _ => BoardLayer::User(s.to_string()), 229 | } 230 | } 231 | } 232 | 233 | impl std::str::FromStr for BoardLayer { 234 | type Err = (); 235 | 236 | fn from_str(s: &str) -> std::result::Result { 237 | Ok(Self::from(s)) 238 | } 239 | } 240 | 241 | impl<'a> From<&'a BoardLayer> for &'a str { 242 | fn from(layer: &'a BoardLayer) -> Self { 243 | match *layer { 244 | BoardLayer::FCu => "F.Cu", 245 | BoardLayer::BCu => "B.Cu", 246 | BoardLayer::FAdhes => "F.Adhes", 247 | BoardLayer::BAdhes => "B.Adhes", 248 | BoardLayer::FPaste => "F.Paste", 249 | BoardLayer::BPaste => "B.Paste", 250 | BoardLayer::FSilkS => "F.SilkS", 251 | BoardLayer::BSilkS => "B.SilkS", 252 | BoardLayer::FMask => "F.Mask", 253 | BoardLayer::BFask => "B.Mask", 254 | BoardLayer::DwgsUser => "Dwgs.User", 255 | BoardLayer::CmtsUser => "Cmts.User", 256 | BoardLayer::Eco1User => "Eco1.User", 257 | BoardLayer::Eco2User => "Eco2.User", 258 | BoardLayer::EdgeCuts => "Edge.Cuts", 259 | BoardLayer::Margin => "Margin", 260 | BoardLayer::BCrtYd => "B.CrtYd", 261 | BoardLayer::FCrtYd => "F.CrtYd", 262 | BoardLayer::BFab => "B.Fab", 263 | BoardLayer::FFab => "F.Fab", 264 | BoardLayer::In1Cu => "In1.Cu", 265 | BoardLayer::In2Cu => "In2.Cu", 266 | BoardLayer::In3Cu => "In3.Cu", 267 | BoardLayer::In4Cu => "In4.Cu", 268 | BoardLayer::User(ref s) => s, 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /crates/kicad-parser/src/graphics.rs: -------------------------------------------------------------------------------- 1 | use crate::{extract_coords, Error}; 2 | use sexp::{Atom, Sexp}; 3 | 4 | use crate::board::BoardLayer; 5 | 6 | #[derive(Debug, Clone, PartialEq)] 7 | pub struct GraphicLine { 8 | pub start_point: (f64, f64), 9 | pub end_point: (f64, f64), 10 | pub layer: BoardLayer, 11 | } 12 | 13 | impl GraphicLine { 14 | pub fn from_list(list: &[Sexp]) -> Result { 15 | let mut start_point: Option<(f64, f64)> = None; 16 | let mut end_point: Option<(f64, f64)> = None; 17 | let mut layer: Option = None; 18 | 19 | for field in list { 20 | let Sexp::List(list) = field else { 21 | continue; 22 | }; 23 | 24 | let Sexp::Atom(Atom::S(head)) = &list[0] else { 25 | continue; 26 | }; 27 | 28 | let rest = &list[1..]; 29 | 30 | match head.as_str() { 31 | "start" => { 32 | let coords = extract_coords(&rest[0], &rest[1])?; 33 | start_point = Some(coords); 34 | }, 35 | "end" => { 36 | let coords = extract_coords(&rest[0], &rest[1])?; 37 | end_point = Some(coords); 38 | }, 39 | "layer" => { 40 | if let Sexp::Atom(Atom::S(layer_str)) = &rest[0] { 41 | let layer_valid = layer_str.as_str().into(); 42 | layer = Some(layer_valid); 43 | } 44 | }, 45 | _ => {}, 46 | } 47 | } 48 | 49 | if let (Some(start_point), Some(end_point), Some(layer)) = (start_point, end_point, layer) { 50 | Ok(Self { start_point, end_point, layer }) 51 | } else { 52 | Err(Error::IncompleteGraphicLine(list.to_vec())) 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone, PartialEq)] 58 | pub struct GraphicArc { 59 | pub start_point: (f64, f64), 60 | pub mid_point: (f64, f64), 61 | pub end_point: (f64, f64), 62 | pub layer: BoardLayer, 63 | } 64 | 65 | impl GraphicArc { 66 | pub fn from_list(list: &[Sexp]) -> Result { 67 | let mut start_point: Option<(f64, f64)> = None; 68 | let mut mid_point: Option<(f64, f64)> = None; 69 | let mut end_point: Option<(f64, f64)> = None; 70 | let mut layer: Option = None; 71 | 72 | for field in list { 73 | let Sexp::List(list) = field else { 74 | continue; 75 | }; 76 | 77 | let Sexp::Atom(Atom::S(head)) = &list[0] else { 78 | continue; 79 | }; 80 | 81 | let rest = &list[1..]; 82 | 83 | match head.as_str() { 84 | "start" => { 85 | let coords = extract_coords(&rest[0], &rest[1])?; 86 | start_point = Some(coords); 87 | }, 88 | "mid" => { 89 | let coords = extract_coords(&rest[0], &rest[1])?; 90 | mid_point = Some(coords); 91 | }, 92 | "end" => { 93 | let coords = extract_coords(&rest[0], &rest[1])?; 94 | end_point = Some(coords); 95 | }, 96 | "layer" => { 97 | if let Sexp::Atom(Atom::S(layer_str)) = &rest[0] { 98 | layer = Some(layer_str.as_str().into()); 99 | } 100 | }, 101 | _ => {}, 102 | } 103 | } 104 | 105 | if let (Some(start_point), Some(mid_point), Some(end_point), Some(layer)) = 106 | (start_point, mid_point, end_point, layer) 107 | { 108 | Ok(Self { start_point, mid_point, end_point, layer }) 109 | } else { 110 | Err(Error::IncompleteGraphicArc(list.to_vec())) 111 | } 112 | } 113 | } 114 | 115 | #[derive(Debug, Clone, PartialEq)] 116 | pub struct GraphicCircle { 117 | pub center_point: (f64, f64), 118 | pub end_point: (f64, f64), 119 | pub layer: BoardLayer, 120 | } 121 | 122 | impl GraphicCircle { 123 | pub fn from_list(list: &[Sexp]) -> Result { 124 | let mut center_point: Option<(f64, f64)> = None; 125 | let mut end_point: Option<(f64, f64)> = None; 126 | let mut layer: Option = None; 127 | 128 | for field in list { 129 | let Sexp::List(list) = field else { 130 | continue; 131 | }; 132 | 133 | let Sexp::Atom(Atom::S(head)) = &list[0] else { 134 | continue; 135 | }; 136 | 137 | let rest = &list[1..]; 138 | 139 | match head.as_str() { 140 | "center" => { 141 | let coords = extract_coords(&rest[0], &rest[1])?; 142 | center_point = Some(coords); 143 | }, 144 | "end" => { 145 | let coords = extract_coords(&rest[0], &rest[1])?; 146 | end_point = Some(coords); 147 | }, 148 | "layer" => { 149 | if let Sexp::Atom(Atom::S(layer_str)) = &rest[0] { 150 | layer = Some(layer_str.as_str().into()); 151 | } 152 | }, 153 | _ => {}, 154 | } 155 | } 156 | 157 | if let (Some(center_point), Some(end_point), Some(layer)) = (center_point, end_point, layer) 158 | { 159 | Ok(Self { center_point, end_point, layer }) 160 | } else { 161 | Err(Error::IncompleteGraphicCircle(list.to_vec())) 162 | } 163 | } 164 | } 165 | 166 | #[derive(Debug, Clone, PartialEq)] 167 | pub struct GraphicRect { 168 | pub start_point: (f64, f64), 169 | pub end_point: (f64, f64), 170 | pub layer: BoardLayer, 171 | } 172 | 173 | impl GraphicRect { 174 | pub fn from_list(list: &[Sexp]) -> Result { 175 | let mut start_point: Option<(f64, f64)> = None; 176 | let mut end_point: Option<(f64, f64)> = None; 177 | let mut layer: Option = None; 178 | 179 | for field in list { 180 | let Sexp::List(list) = field else { 181 | continue; 182 | }; 183 | 184 | let Sexp::Atom(Atom::S(head)) = &list[0] else { 185 | continue; 186 | }; 187 | 188 | let rest = &list[1..]; 189 | 190 | match head.as_str() { 191 | "start" => { 192 | let coords = extract_coords(&rest[0], &rest[1])?; 193 | start_point = Some(coords); 194 | }, 195 | "end" => { 196 | let coords = extract_coords(&rest[0], &rest[1])?; 197 | end_point = Some(coords); 198 | }, 199 | "layer" => { 200 | if let Sexp::Atom(Atom::S(layer_str)) = &rest[0] { 201 | layer = Some(layer_str.as_str().into()); 202 | } 203 | }, 204 | _ => {}, 205 | } 206 | } 207 | 208 | if let (Some(start_point), Some(end_point), Some(layer)) = (start_point, end_point, layer) { 209 | Ok(Self { start_point, end_point, layer }) 210 | } else { 211 | Err(Error::IncompleteGraphicRect(list.to_vec())) 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /crates/kicad-parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | use sexp::{Atom, Sexp}; 2 | use thiserror::Error; 3 | 4 | pub mod board; 5 | pub mod graphics; 6 | 7 | #[derive(Error, Debug)] 8 | pub enum Error { 9 | #[error("IO Error: {0}")] 10 | IoError(#[from] std::io::Error), 11 | #[error("S-Expression Parse Error: {0}")] 12 | SexpParseError(#[from] Box), 13 | #[error("Top level object is not a list")] 14 | TopLevelObjectNotList, 15 | #[error("First element in the top level list should be a string")] 16 | FirstElementInListNotString, 17 | #[error("The file is not a kicad_pcb file")] 18 | NotKicadPcbFile, 19 | #[error("Tried to extract a number which is not a float or an int")] 20 | NumberShouldBeFloatOrInt, 21 | #[error("Incomplete GraphicLine: {0:?}")] 22 | IncompleteGraphicLine(Vec), 23 | #[error("Incomplete GraphicArc: {0:?}")] 24 | IncompleteGraphicArc(Vec), 25 | #[error("Incomplete GraphicCircle: {0:?}")] 26 | IncompleteGraphicCircle(Vec), 27 | #[error("Incomplete GraphicRect: {0:?}")] 28 | IncompleteGraphicRect(Vec), 29 | } 30 | 31 | fn extract_number(num: &Sexp) -> Result { 32 | match num { 33 | Sexp::Atom(Atom::F(float)) => Ok(*float), 34 | Sexp::Atom(Atom::I(int)) => Ok(*int as f64), 35 | _ => Err(Error::NumberShouldBeFloatOrInt), 36 | } 37 | } 38 | 39 | fn extract_coords(x: &Sexp, y: &Sexp) -> Result<(f64, f64), Error> { 40 | Ok((extract_number(x)?, extract_number(y)?)) 41 | } 42 | -------------------------------------------------------------------------------- /crates/model-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "model-api" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wit-bindgen = "0.41" 8 | glam = { version = "0.24", features = ["bytemuck"] } 9 | -------------------------------------------------------------------------------- /crates/model-api/src/angle.rs: -------------------------------------------------------------------------------- 1 | use glam::{dvec3, DVec3}; 2 | use std::ops::{Div, Mul}; 3 | 4 | #[derive(Debug, Copy, Clone)] 5 | pub enum Angle { 6 | Radians(f64), 7 | Degrees(f64), 8 | } 9 | 10 | impl Angle { 11 | pub fn radians(self) -> f64 { 12 | match self { 13 | Self::Radians(r) => r, 14 | Self::Degrees(d) => (d * std::f64::consts::PI) / 180.0, 15 | } 16 | } 17 | 18 | pub fn degrees(self) -> f64 { 19 | match self { 20 | Self::Radians(r) => (r * 180.0) / std::f64::consts::PI, 21 | Self::Degrees(d) => d, 22 | } 23 | } 24 | } 25 | 26 | impl Mul for Angle { 27 | type Output = Angle; 28 | 29 | fn mul(self, multiplier: f64) -> Self::Output { 30 | match self { 31 | Self::Radians(angle) => Self::Radians(angle * multiplier), 32 | Self::Degrees(angle) => Self::Degrees(angle * multiplier), 33 | } 34 | } 35 | } 36 | 37 | impl Div for Angle { 38 | type Output = Angle; 39 | 40 | fn div(self, divisor: f64) -> Self::Output { 41 | match self { 42 | Self::Radians(angle) => Self::Radians(angle / divisor), 43 | Self::Degrees(angle) => Self::Degrees(angle / divisor), 44 | } 45 | } 46 | } 47 | 48 | pub trait ToAngle { 49 | fn degrees(&self) -> Angle; 50 | fn radians(&self) -> Angle; 51 | } 52 | 53 | impl + Copy> ToAngle for T { 54 | fn degrees(&self) -> Angle { 55 | Angle::Degrees((*self).into()) 56 | } 57 | 58 | fn radians(&self) -> Angle { 59 | Angle::Radians((*self).into()) 60 | } 61 | } 62 | 63 | /// Represents rotation on the X, Y, and Z axes. Also known 64 | /// as Euler angle representation. 65 | #[derive(Debug, Copy, Clone)] 66 | pub struct RVec { 67 | pub x: Angle, 68 | pub y: Angle, 69 | pub z: Angle, 70 | } 71 | 72 | impl RVec { 73 | pub fn radians(&self) -> DVec3 { 74 | dvec3(self.x.radians(), self.y.radians(), self.z.radians()) 75 | } 76 | 77 | pub fn degrees(&self) -> DVec3 { 78 | dvec3(self.x.degrees(), self.y.degrees(), self.z.degrees()) 79 | } 80 | 81 | pub fn x(x: Angle) -> Self { 82 | RVec { x, y: 0.degrees(), z: 0.degrees() } 83 | } 84 | 85 | pub fn y(y: Angle) -> Self { 86 | RVec { x: 0.degrees(), y, z: 0.degrees() } 87 | } 88 | 89 | pub fn z(z: Angle) -> Self { 90 | RVec { x: 0.degrees(), y: 0.degrees(), z } 91 | } 92 | } 93 | 94 | pub fn rvec(x: Angle, y: Angle, z: Angle) -> RVec { 95 | RVec { x, y, z } 96 | } 97 | -------------------------------------------------------------------------------- /crates/model-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::Shape; 2 | 3 | pub mod angle; 4 | pub mod primitives; 5 | pub mod wasm; 6 | pub mod workplane; 7 | 8 | pub trait Model: Send + Sync { 9 | fn new() -> Self 10 | where 11 | Self: Sized; 12 | 13 | fn create_model(&mut self) -> Shape; 14 | } 15 | -------------------------------------------------------------------------------- /crates/model-api/src/primitives.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm; 2 | use glam::DVec3; 3 | 4 | mod compound; 5 | mod edge; 6 | mod face; 7 | mod shape; 8 | mod shell; 9 | mod solid; 10 | mod wire; 11 | 12 | pub use compound::*; 13 | pub use edge::*; 14 | pub use face::*; 15 | pub use shape::*; 16 | pub use shell::*; 17 | pub use solid::*; 18 | pub use wire::*; 19 | 20 | pub trait IntoShape { 21 | fn into_shape(self) -> Shape; 22 | } 23 | 24 | impl> IntoShape for T { 25 | fn into_shape(self) -> Shape { 26 | self.into() 27 | } 28 | } 29 | 30 | #[derive(Debug, Copy, Clone)] 31 | pub enum Direction { 32 | PosX, 33 | NegX, 34 | PosY, 35 | NegY, 36 | PosZ, 37 | NegZ, 38 | Custom(DVec3), 39 | } 40 | 41 | impl Direction { 42 | pub fn normalized_vec(&self) -> DVec3 { 43 | match self { 44 | Self::PosX => DVec3::X, 45 | Self::NegX => DVec3::NEG_X, 46 | Self::PosY => DVec3::Y, 47 | Self::NegY => DVec3::NEG_Y, 48 | Self::PosZ => DVec3::Z, 49 | Self::NegZ => DVec3::NEG_Z, 50 | Self::Custom(dir) => dir.normalize(), 51 | } 52 | } 53 | } 54 | 55 | pub struct EdgeIterator { 56 | iterator: wasm::EdgeIterator, 57 | } 58 | 59 | impl EdgeIterator { 60 | pub fn new(face: &Face) -> Self { 61 | Self { iterator: wasm::EdgeIterator::new(&face.inner) } 62 | } 63 | } 64 | 65 | impl Iterator for EdgeIterator { 66 | type Item = Edge; 67 | 68 | fn next(&mut self) -> Option { 69 | self.iterator.next().map(|inner_edge| Edge { inner: inner_edge }) 70 | } 71 | } 72 | 73 | pub struct FaceIterator { 74 | iterator: wasm::FaceIterator, 75 | } 76 | 77 | impl FaceIterator { 78 | pub fn new(shape: &Shape) -> Self { 79 | Self { iterator: wasm::FaceIterator::new(&shape.inner) } 80 | } 81 | 82 | pub fn farthest(self, direction: Direction) -> Face { 83 | self.try_farthest(direction).unwrap() 84 | } 85 | 86 | pub fn try_farthest(self, direction: Direction) -> Option { 87 | let normalized_dir = direction.normalized_vec(); 88 | 89 | self.max_by(|face_1, face_2| { 90 | let dist_1 = face_1.center_of_mass().dot(normalized_dir); 91 | let dist_2 = face_2.center_of_mass().dot(normalized_dir); 92 | 93 | PartialOrd::partial_cmp(&dist_1, &dist_2) 94 | .expect("Face center of masses should contain no NaNs") 95 | }) 96 | } 97 | } 98 | 99 | impl Iterator for FaceIterator { 100 | type Item = Face; 101 | 102 | fn next(&mut self) -> Option { 103 | self.iterator.next().map(|inner_face| Face { inner: inner_face }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /crates/model-api/src/primitives/compound.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm; 2 | 3 | pub struct Compound { 4 | pub(crate) inner: wasm::Compound, 5 | } 6 | 7 | impl AsRef for Compound { 8 | fn as_ref(&self) -> &Compound { 9 | self 10 | } 11 | } 12 | 13 | impl Compound {} 14 | -------------------------------------------------------------------------------- /crates/model-api/src/primitives/edge.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm; 2 | use glam::DVec3; 3 | 4 | pub struct Edge { 5 | pub(crate) inner: wasm::Edge, 6 | } 7 | 8 | impl AsRef for Edge { 9 | fn as_ref(&self) -> &Edge { 10 | self 11 | } 12 | } 13 | 14 | impl Edge { 15 | pub fn segment(p1: DVec3, p2: DVec3) -> Self { 16 | let inner = wasm::Edge::segment(p1.into(), p2.into()); 17 | 18 | Edge { inner } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/model-api/src/primitives/face.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | primitives::{EdgeIterator, Solid, Wire}, 3 | wasm, 4 | }; 5 | use glam::DVec3; 6 | 7 | pub struct Face { 8 | pub(crate) inner: wasm::Face, 9 | } 10 | 11 | impl AsRef for Face { 12 | fn as_ref(&self) -> &Face { 13 | self 14 | } 15 | } 16 | 17 | impl Face { 18 | #[must_use] 19 | pub fn from_wire(wire: &Wire) -> Self { 20 | let host_face = wasm::Face::from_wire(&wire.inner); 21 | 22 | Self { inner: host_face } 23 | } 24 | 25 | #[must_use] 26 | pub fn fillet(&self, radius: f64) -> Self { 27 | let host_face = self.inner.fillet(radius); 28 | 29 | Self { inner: host_face } 30 | } 31 | 32 | #[must_use] 33 | pub fn extrude(&self, dir: DVec3) -> Solid { 34 | let host_solid = self.inner.extrude(dir.into()); 35 | 36 | Solid { inner: host_solid } 37 | } 38 | 39 | pub fn outer_wire(&self) -> Wire { 40 | let host_wire = self.inner.outer_wire(); 41 | 42 | Wire { inner: host_wire } 43 | } 44 | 45 | pub fn center_of_mass(&self) -> DVec3 { 46 | self.inner.center_of_mass().into() 47 | } 48 | 49 | pub fn edges(&self) -> EdgeIterator { 50 | EdgeIterator::new(self) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/model-api/src/primitives/shape.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | primitives::{Compound, Edge, Face, FaceIterator, Shell, Solid, Wire}, 3 | wasm, 4 | }; 5 | 6 | pub struct Shape { 7 | pub(crate) inner: wasm::Shape, 8 | } 9 | 10 | impl AsRef for Shape { 11 | fn as_ref(&self) -> &Shape { 12 | self 13 | } 14 | } 15 | 16 | impl From for Shape { 17 | fn from(edge: Edge) -> Self { 18 | Shape::from_edge(&edge) 19 | } 20 | } 21 | 22 | impl From<&Edge> for Shape { 23 | fn from(edge: &Edge) -> Self { 24 | Shape::from_edge(edge) 25 | } 26 | } 27 | 28 | impl From for Shape { 29 | fn from(wire: Wire) -> Self { 30 | Shape::from_wire(&wire) 31 | } 32 | } 33 | 34 | impl From<&Wire> for Shape { 35 | fn from(wire: &Wire) -> Self { 36 | Shape::from_wire(wire) 37 | } 38 | } 39 | 40 | impl From for Shape { 41 | fn from(face: Face) -> Self { 42 | Shape::from_face(&face) 43 | } 44 | } 45 | 46 | impl From<&Face> for Shape { 47 | fn from(face: &Face) -> Self { 48 | Shape::from_face(face) 49 | } 50 | } 51 | 52 | impl From for Shape { 53 | fn from(shell: Shell) -> Self { 54 | Shape::from_shell(&shell) 55 | } 56 | } 57 | 58 | impl From<&Shell> for Shape { 59 | fn from(shell: &Shell) -> Self { 60 | Shape::from_shell(shell) 61 | } 62 | } 63 | 64 | impl From for Shape { 65 | fn from(solid: Solid) -> Self { 66 | Shape::from_solid(&solid) 67 | } 68 | } 69 | 70 | impl From<&Solid> for Shape { 71 | fn from(solid: &Solid) -> Self { 72 | Shape::from_solid(solid) 73 | } 74 | } 75 | 76 | impl From for Shape { 77 | fn from(compound: Compound) -> Self { 78 | Shape::from_compound(&compound) 79 | } 80 | } 81 | 82 | impl From<&Compound> for Shape { 83 | fn from(compound: &Compound) -> Self { 84 | Shape::from_compound(compound) 85 | } 86 | } 87 | 88 | impl Shape { 89 | pub fn from_edge(edge: &Edge) -> Self { 90 | let shape = wasm::Shape::from_edge(&edge.inner); 91 | 92 | Self { inner: shape } 93 | } 94 | 95 | pub fn from_wire(wire: &Wire) -> Self { 96 | let shape = wasm::Shape::from_wire(&wire.inner); 97 | 98 | Self { inner: shape } 99 | } 100 | 101 | pub fn from_face(face: &Face) -> Self { 102 | let shape = wasm::Shape::from_face(&face.inner); 103 | 104 | Self { inner: shape } 105 | } 106 | 107 | pub fn from_shell(shell: &Shell) -> Self { 108 | let shape = wasm::Shape::from_shell(&shell.inner); 109 | 110 | Self { inner: shape } 111 | } 112 | 113 | pub fn from_solid(solid: &Solid) -> Self { 114 | let shape = wasm::Shape::from_solid(&solid.inner); 115 | 116 | Self { inner: shape } 117 | } 118 | 119 | pub fn from_compound(compound: &Compound) -> Self { 120 | let shape = wasm::Shape::from_compound(&compound.inner); 121 | 122 | Self { inner: shape } 123 | } 124 | 125 | #[must_use] 126 | pub fn chamfer_edges>( 127 | &self, 128 | distance: f64, 129 | edges: impl IntoIterator, 130 | ) -> Self { 131 | let make_chamfer = wasm::ChamferMaker::new(&self.inner); 132 | 133 | for edge in edges.into_iter() { 134 | make_chamfer.add_edge(distance, &edge.as_ref().inner); 135 | } 136 | 137 | Self { inner: make_chamfer.build() } 138 | } 139 | 140 | pub fn faces(&self) -> FaceIterator { 141 | FaceIterator::new(self) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /crates/model-api/src/primitives/shell.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm; 2 | 3 | pub struct Shell { 4 | pub(crate) inner: wasm::Shell, 5 | } 6 | 7 | impl AsRef for Shell { 8 | fn as_ref(&self) -> &Shell { 9 | self 10 | } 11 | } 12 | 13 | impl Shell {} 14 | -------------------------------------------------------------------------------- /crates/model-api/src/primitives/solid.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm; 2 | 3 | pub struct Solid { 4 | pub(crate) inner: wasm::Solid, 5 | } 6 | 7 | impl AsRef for Solid { 8 | fn as_ref(&self) -> &Solid { 9 | self 10 | } 11 | } 12 | 13 | impl Solid {} 14 | -------------------------------------------------------------------------------- /crates/model-api/src/primitives/wire.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | primitives::{Edge, Face}, 3 | wasm, 4 | }; 5 | 6 | pub struct Wire { 7 | pub(crate) inner: wasm::Wire, 8 | } 9 | 10 | impl AsRef for Wire { 11 | fn as_ref(&self) -> &Wire { 12 | self 13 | } 14 | } 15 | 16 | impl Wire { 17 | pub fn from_edges<'a>(edges: impl IntoIterator) -> Self { 18 | let wire_builder = wasm::WireBuilder::new(); 19 | 20 | for edge in edges.into_iter() { 21 | wire_builder.add_edge(&edge.inner); 22 | } 23 | 24 | Self { inner: wire_builder.build() } 25 | } 26 | 27 | pub fn fillet(&self, radius: f64) -> Self { 28 | let face = Face::from_wire(self).fillet(radius); 29 | face.outer_wire() 30 | } 31 | 32 | pub fn to_face(&self) -> Face { 33 | Face::from_wire(self) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/model-api/src/wasm.rs: -------------------------------------------------------------------------------- 1 | use crate::Model; 2 | use glam::DVec3; 3 | 4 | // Use a procedural macro to generate bindings for the world we specified in 5 | // `host.wit` 6 | wit_bindgen::generate!({ 7 | // the name of the world in the `*.wit` input file 8 | world: "model-world", 9 | // "init-model" is skipped because it is exported manually below. 10 | skip: ["init-model"], 11 | }); 12 | 13 | impl From for Point3 { 14 | fn from(p: DVec3) -> Self { 15 | Self { x: p.x, y: p.y, z: p.z } 16 | } 17 | } 18 | 19 | impl From for DVec3 { 20 | fn from(p: Point3) -> Self { 21 | Self { x: p.x, y: p.y, z: p.z } 22 | } 23 | } 24 | 25 | // Define a custom type and implement the generated `Guest` trait for it which 26 | // represents implementing all the necessary exported interfaces for this 27 | // component. 28 | pub struct CadModelCode; 29 | 30 | impl Guest for CadModelCode { 31 | fn run() -> Shape { 32 | print("Hello, world!"); 33 | let user_shape = model().create_model(); 34 | 35 | user_shape.inner 36 | } 37 | } 38 | 39 | static mut MODEL: Option> = None; 40 | 41 | #[doc(hidden)] 42 | pub fn register_model(build_model: fn() -> Box) { 43 | unsafe { MODEL = Some((build_model)()) } 44 | } 45 | 46 | fn model() -> &'static mut dyn Model { 47 | // TODO(bschwind) - Use something with interior mutability if possible. 48 | #[allow(static_mut_refs)] 49 | unsafe { 50 | MODEL.as_deref_mut().unwrap() 51 | } 52 | } 53 | 54 | #[macro_export] 55 | macro_rules! register_model { 56 | ($model_type:ty) => { 57 | #[export_name = "init-model"] 58 | pub extern "C" fn __init_model() { 59 | model_api::wasm::register_model(|| Box::new(<$model_type as model_api::Model>::new())); 60 | } 61 | }; 62 | } 63 | 64 | // export! defines that the `CadModelCode` struct defined below is going to define 65 | // the exports of the `world`, namely the `run` function. 66 | export!(CadModelCode); 67 | -------------------------------------------------------------------------------- /crates/model-api/src/workplane.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | angle::{Angle, RVec}, 3 | primitives::{Edge, Wire}, 4 | }; 5 | use glam::{dvec3, DAffine3, DMat3, DVec3, EulerRot}; 6 | 7 | #[derive(Debug, Copy, Clone)] 8 | pub enum Plane { 9 | XY, 10 | YZ, 11 | ZX, 12 | XZ, 13 | YX, 14 | ZY, 15 | Front, 16 | Back, 17 | Left, 18 | Right, 19 | Top, 20 | Bottom, 21 | Custom { x_dir: (f64, f64, f64), normal_dir: (f64, f64, f64) }, 22 | } 23 | 24 | impl Plane { 25 | pub fn transform_point(&self, point: DVec3) -> DVec3 { 26 | self.transform().transform_point3(point) 27 | } 28 | 29 | pub fn transform(&self) -> DAffine3 { 30 | match self { 31 | Self::XY => DAffine3::from_cols(DVec3::X, DVec3::Y, DVec3::Z, DVec3::ZERO), 32 | Self::YZ => DAffine3::from_cols(DVec3::Y, DVec3::Z, DVec3::X, DVec3::ZERO), 33 | Self::ZX => DAffine3::from_cols(DVec3::Z, DVec3::X, DVec3::Y, DVec3::ZERO), 34 | Self::XZ => DAffine3::from_cols(DVec3::X, DVec3::Z, DVec3::NEG_Y, DVec3::ZERO), 35 | Self::YX => DAffine3::from_cols(DVec3::Y, DVec3::X, DVec3::NEG_Z, DVec3::ZERO), 36 | Self::ZY => DAffine3::from_cols(DVec3::Z, DVec3::Y, DVec3::NEG_X, DVec3::ZERO), 37 | Self::Front => DAffine3::from_cols(DVec3::X, DVec3::Y, DVec3::Z, DVec3::ZERO), 38 | Self::Back => DAffine3::from_cols(DVec3::NEG_X, DVec3::Y, DVec3::NEG_Z, DVec3::ZERO), 39 | Self::Left => DAffine3::from_cols(DVec3::Z, DVec3::Y, DVec3::NEG_X, DVec3::ZERO), 40 | Self::Right => DAffine3::from_cols(DVec3::NEG_Z, DVec3::Y, DVec3::X, DVec3::ZERO), 41 | Self::Top => DAffine3::from_cols(DVec3::X, DVec3::NEG_Z, DVec3::Y, DVec3::ZERO), 42 | Self::Bottom => DAffine3::from_cols(DVec3::X, DVec3::Z, DVec3::NEG_Y, DVec3::ZERO), 43 | Self::Custom { x_dir, normal_dir } => { 44 | let x_axis = dvec3(x_dir.0, x_dir.1, x_dir.2).normalize(); 45 | let z_axis = dvec3(normal_dir.0, normal_dir.1, normal_dir.2).normalize(); 46 | let y_axis = z_axis.cross(x_axis).normalize(); 47 | 48 | DAffine3::from_cols(x_axis, y_axis, z_axis, DVec3::ZERO) 49 | }, 50 | } 51 | } 52 | } 53 | 54 | #[derive(Debug, Clone)] 55 | pub struct Workplane { 56 | transform: DAffine3, 57 | } 58 | 59 | impl Workplane { 60 | pub fn new(x_dir: DVec3, normal_dir: DVec3) -> Self { 61 | Self { 62 | transform: Plane::Custom { 63 | x_dir: x_dir.normalize().into(), 64 | normal_dir: normal_dir.normalize().into(), 65 | } 66 | .transform(), 67 | } 68 | } 69 | 70 | pub fn xy() -> Self { 71 | Self { transform: Plane::XY.transform() } 72 | } 73 | 74 | pub fn yz() -> Self { 75 | Self { transform: Plane::YZ.transform() } 76 | } 77 | 78 | pub fn zx() -> Self { 79 | Self { transform: Plane::ZX.transform() } 80 | } 81 | 82 | pub fn xz() -> Self { 83 | Self { transform: Plane::XZ.transform() } 84 | } 85 | 86 | pub fn zy() -> Self { 87 | Self { transform: Plane::ZY.transform() } 88 | } 89 | 90 | pub fn yx() -> Self { 91 | Self { transform: Plane::YX.transform() } 92 | } 93 | 94 | pub fn origin(&self) -> DVec3 { 95 | self.transform.translation 96 | } 97 | 98 | pub fn normal(&self) -> DVec3 { 99 | self.transform.matrix3.z_axis 100 | } 101 | 102 | pub fn x_dir(&self) -> DVec3 { 103 | self.transform.matrix3.x_axis 104 | } 105 | 106 | pub fn y_dir(&self) -> DVec3 { 107 | self.transform.matrix3.y_axis 108 | } 109 | 110 | // TODO(bschwind) - Test this. 111 | pub fn set_rotation(&mut self, (rot_x, rot_y, rot_z): (Angle, Angle, Angle)) { 112 | let rotation_matrix = 113 | DMat3::from_euler(EulerRot::XYZ, rot_x.radians(), rot_y.radians(), rot_z.radians()); 114 | 115 | let translation = self.transform.translation; 116 | 117 | let x_dir = DVec3::X; 118 | let normal_dir = DVec3::Z; 119 | 120 | self.transform = Plane::Custom { 121 | x_dir: rotation_matrix.mul_vec3(x_dir).into(), 122 | normal_dir: rotation_matrix.mul_vec3(normal_dir).into(), 123 | } 124 | .transform(); 125 | 126 | self.set_translation(translation); 127 | } 128 | 129 | pub fn rotate_by(&mut self, (rot_x, rot_y, rot_z): (Angle, Angle, Angle)) { 130 | let rotation_matrix = 131 | DMat3::from_euler(EulerRot::XYZ, rot_x.radians(), rot_y.radians(), rot_z.radians()); 132 | 133 | let translation = self.transform.translation; 134 | 135 | let x_dir = rotation_matrix.mul_vec3(DVec3::X); 136 | let normal_dir = rotation_matrix.mul_vec3(DVec3::Z); 137 | 138 | self.transform = Plane::Custom { 139 | x_dir: self.transform.transform_vector3(x_dir).into(), 140 | normal_dir: self.transform.transform_vector3(normal_dir).into(), 141 | } 142 | .transform(); 143 | 144 | self.set_translation(translation); 145 | } 146 | 147 | pub fn set_translation(&mut self, pos: DVec3) { 148 | self.transform.translation = pos; 149 | } 150 | 151 | pub fn translate_by(&mut self, offset: DVec3) { 152 | self.transform.translation += offset; 153 | } 154 | 155 | pub fn transformed(&self, offset: DVec3, rotate: RVec) -> Self { 156 | let mut new = self.clone(); 157 | let new_origin = new.to_world_pos(offset); 158 | 159 | new.rotate_by((rotate.x, rotate.y, rotate.z)); 160 | new.transform.translation = new_origin; 161 | 162 | new 163 | } 164 | 165 | pub fn translated(&self, offset: DVec3) -> Self { 166 | let mut new = self.clone(); 167 | let new_origin = new.to_world_pos(offset); 168 | new.transform.translation = new_origin; 169 | 170 | new 171 | } 172 | 173 | pub fn rotated(&self, rotate: RVec) -> Self { 174 | let mut new = self.clone(); 175 | new.rotate_by((rotate.x, rotate.y, rotate.z)); 176 | 177 | new 178 | } 179 | 180 | pub fn to_world_pos(&self, pos: DVec3) -> DVec3 { 181 | self.transform.transform_point3(pos) 182 | } 183 | 184 | pub fn to_local_pos(&self, pos: DVec3) -> DVec3 { 185 | self.transform.inverse().transform_point3(pos) 186 | } 187 | 188 | pub fn rect(&self, width: f64, height: f64) -> Wire { 189 | let half_width = width / 2.0; 190 | let half_height = height / 2.0; 191 | 192 | let p1 = self.to_world_pos(dvec3(-half_width, half_height, 0.0)); 193 | let p2 = self.to_world_pos(dvec3(half_width, half_height, 0.0)); 194 | let p3 = self.to_world_pos(dvec3(half_width, -half_height, 0.0)); 195 | let p4 = self.to_world_pos(dvec3(-half_width, -half_height, 0.0)); 196 | 197 | let top = Edge::segment(p1, p2); 198 | let right = Edge::segment(p2, p3); 199 | let bottom = Edge::segment(p3, p4); 200 | let left = Edge::segment(p4, p1); 201 | 202 | Wire::from_edges([&top, &right, &bottom, &left]) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /crates/model-api/wit/model-api.wit: -------------------------------------------------------------------------------- 1 | package example:host; 2 | 3 | world model-world { 4 | import print: func(msg: string); 5 | 6 | record point3 { 7 | x: f64, 8 | y: f64, 9 | z: f64, 10 | } 11 | 12 | resource edge-iterator { 13 | constructor(face: borrow); 14 | next: func() -> option; 15 | } 16 | 17 | resource face-iterator { 18 | constructor(shape: borrow); 19 | next: func() -> option; 20 | } 21 | 22 | resource chamfer-maker { 23 | constructor(shape: borrow); 24 | add-edge: func(distance: f64, edge: borrow); 25 | build: func() -> shape; 26 | } 27 | 28 | resource wire-builder { 29 | constructor(); 30 | add-edge: func(edge: borrow); 31 | build: func() -> wire; 32 | } 33 | 34 | resource wire { 35 | 36 | } 37 | 38 | resource face { 39 | from-wire: static func(w: borrow) -> face; 40 | fillet: func(radius: f64) -> face; 41 | extrude: func(dir: point3) -> solid; 42 | outer-wire: func() -> wire; 43 | center-of-mass: func() -> point3; 44 | } 45 | 46 | resource shell { 47 | 48 | } 49 | 50 | resource solid { 51 | 52 | } 53 | 54 | resource compound { 55 | 56 | } 57 | 58 | resource edge { 59 | segment: static func(p1: point3, p2: point3) -> edge; 60 | } 61 | 62 | resource shape { 63 | from-edge: static func(w: borrow) -> shape; 64 | from-wire: static func(w: borrow) -> shape; 65 | from-face: static func(w: borrow) -> shape; 66 | from-shell: static func(w: borrow) -> shape; 67 | from-solid: static func(w: borrow) -> shape; 68 | from-compound: static func(w: borrow) -> shape; 69 | } 70 | 71 | export init-model: func(); 72 | export run: func() -> shape; 73 | } 74 | -------------------------------------------------------------------------------- /crates/occt-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "occt-sys" 3 | description = "Static build of the C++ OpenCascade CAD Kernel for use as a Rust dependency" 4 | authors = ["Brian Schwind ", "Matěj Laitl "] 5 | license = "LGPL-2.1" 6 | version = "0.6.0" 7 | edition = "2021" 8 | repository = "https://github.com/bschwind/opencascade-rs" 9 | 10 | # Reduce the crate size so we can publish on crates.io. 11 | # Sorted by file size 12 | exclude = [ 13 | "OCCT/data/*", 14 | "OCCT/dox/*", 15 | "OCCT/tests/*", 16 | "OCCT/samples/*", 17 | "OCCT/tools/*", 18 | "OCCT/src/Textures/*", 19 | "OCCT/src/OpenGl/*", 20 | "OCCT/src/ViewerTest/*", 21 | "OCCT/src/QABugs/*", 22 | ] 23 | 24 | [dependencies] 25 | cmake = "0.1" 26 | 27 | # Adding an empty workspace table so occt-sys doesn't believe 28 | # it's in the parent workspace. This crate is excluded from 29 | # the top-level workspace because it takes quite awhile to 30 | # build and the crate doesn't change very often. 31 | [workspace] 32 | -------------------------------------------------------------------------------- /crates/occt-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env::var, path::Path}; 2 | 3 | fn main() { 4 | println!( 5 | "cargo:rustc-env=OCCT_SRC_DIR={}", 6 | Path::new(&var("CARGO_MANIFEST_DIR").unwrap()).join("OCCT").to_string_lossy() 7 | ); 8 | println!( 9 | "cargo:rustc-env=OCCT_PATCH_DIR={}", 10 | Path::new(&var("CARGO_MANIFEST_DIR").unwrap()).join("patch").to_string_lossy() 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /crates/occt-sys/examples/print_paths.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("occt_path: {}", occt_sys::occt_path().to_str().unwrap()); 3 | } 4 | -------------------------------------------------------------------------------- /crates/occt-sys/patch/adm/MODULES: -------------------------------------------------------------------------------- 1 | FoundationClasses TKernel TKMath 2 | ModelingData TKG2d TKG3d TKGeomBase TKBRep 3 | ModelingAlgorithms TKGeomAlgo TKTopAlgo TKPrim TKBO TKBool TKHLR TKFillet TKOffset TKFeat TKMesh TKXMesh TKShHealing 4 | ApplicationFramework TKCDF TKLCAF TKCAF TKBinL TKXmlL TKBin TKXml TKStdL TKStd TKTObj TKBinTObj TKXmlTObj TKVCAF 5 | DataExchange TKDE TKXSBase TKDESTEP TKDEIGES TKDESTL TKDEVRML TKDECascade TKDEOBJ TKDEGLTF TKDEPLY TKXCAF TKXmlXCAF TKBinXCAF TKRWMesh 6 | -------------------------------------------------------------------------------- /crates/occt-sys/patch/adm/RESOURCES: -------------------------------------------------------------------------------- 1 | UnitsAPI/Units.dat 2 | -------------------------------------------------------------------------------- /crates/occt-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::var, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | const LIB_DIR: &str = "lib"; 7 | const INCLUDE_DIR: &str = "include"; 8 | 9 | /// Get the path to the OCCT library installation directory to be 10 | /// used in build scripts. 11 | /// 12 | /// Only valid during build (`cargo clean` removes these files). 13 | pub fn occt_path() -> PathBuf { 14 | // moves the output into target/TARGET/OCCT 15 | // this way its less likely to be rebuilt without a cargo clean 16 | Path::new(&var("OUT_DIR").expect("missing OUT_DIR")).join("../../../../OCCT") 17 | } 18 | 19 | /// Build the OCCT library. 20 | pub fn build_occt() { 21 | cmake::Config::new(Path::new(env!("OCCT_SRC_DIR"))) 22 | .define("BUILD_PATCH", Path::new(env!("OCCT_PATCH_DIR"))) 23 | .define("BUILD_LIBRARY_TYPE", "Static") 24 | .define("BUILD_MODULE_ApplicationFramework", "FALSE") 25 | .define("BUILD_MODULE_Draw", "FALSE") 26 | .define("USE_D3D", "FALSE") 27 | .define("USE_DRACO", "FALSE") 28 | .define("USE_EIGEN", "FALSE") 29 | .define("USE_FFMPEG", "FALSE") 30 | .define("USE_FREEIMAGE", "FALSE") 31 | .define("USE_FREETYPE", "FALSE") 32 | .define("USE_GLES2", "FALSE") 33 | .define("USE_OPENGL", "FALSE") 34 | .define("USE_OPENVR", "FALSE") 35 | .define("USE_RAPIDJSON", "FALSE") 36 | .define("USE_TBB", "FALSE") 37 | .define("USE_TCL", "FALSE") 38 | .define("USE_TK", "FALSE") 39 | .define("USE_VTK", "FALSE") 40 | .define("USE_XLIB", "FALSE") 41 | .define("INSTALL_DIR_LIB", LIB_DIR) 42 | .define("INSTALL_DIR_INCLUDE", INCLUDE_DIR) 43 | .profile("Release") 44 | .out_dir(occt_path()) 45 | .build(); 46 | } 47 | -------------------------------------------------------------------------------- /crates/opencascade-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "opencascade-sys" 3 | description = "Rust bindings to the OpenCascade CAD Kernel" 4 | authors = ["Brian Schwind "] 5 | license = "LGPL-2.1" 6 | version = "0.2.0" 7 | edition = "2021" 8 | repository = "https://github.com/bschwind/opencascade-rs" 9 | 10 | [dependencies] 11 | cxx = "1" 12 | 13 | [build-dependencies] 14 | cmake = "0.1" 15 | cxx-build = "1" 16 | occt-sys = { version = "0.6", optional = true } 17 | 18 | [features] 19 | builtin = ["occt-sys"] 20 | -------------------------------------------------------------------------------- /crates/opencascade-sys/OCCT/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.1 FATAL_ERROR) 2 | project (OpenCASCADEPackageConfig) 3 | 4 | # Fix for WASM: https://github.com/emscripten-core/emscripten/issues/19243 5 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE "BOTH" CACHE PATH "") 6 | 7 | find_package (OpenCASCADE REQUIRED) 8 | 9 | file (WRITE ${CMAKE_BINARY_DIR}/occ_info.txt 10 | "VERSION_MAJOR=${OpenCASCADE_MAJOR_VERSION}\n" 11 | "VERSION_MINOR=${OpenCASCADE_MINOR_VERSION}\n" 12 | "INCLUDE_DIR=${OpenCASCADE_INCLUDE_DIR}\n" 13 | "LIBRARY_DIR=${OpenCASCADE_LIBRARY_DIR}\n" 14 | "BUILD_SHARED_LIBS=${OpenCASCADE_BUILD_SHARED_LIBS}\n") 15 | 16 | install (FILES ${CMAKE_BINARY_DIR}/occ_info.txt TYPE DATA) 17 | -------------------------------------------------------------------------------- /crates/opencascade-sys/build.rs: -------------------------------------------------------------------------------- 1 | /// Minimum compatible version of OpenCASCADE library (major, minor) 2 | /// 3 | /// Pre-installed OpenCASCADE library will be checked for compatibility using semver rules. 4 | const OCCT_VERSION: (u8, u8) = (7, 8); 5 | 6 | /// The list of used OpenCASCADE libraries which needs to be linked with. 7 | const OCCT_LIBS: &[&str] = &[ 8 | "TKMath", 9 | "TKernel", 10 | "TKDE", 11 | "TKFeat", 12 | "TKGeomBase", 13 | "TKG2d", 14 | "TKG3d", 15 | "TKTopAlgo", 16 | "TKGeomAlgo", 17 | "TKBRep", 18 | "TKPrim", 19 | "TKDESTEP", 20 | "TKDEIGES", 21 | "TKDESTL", 22 | "TKMesh", 23 | "TKShHealing", 24 | "TKFillet", 25 | "TKBool", 26 | "TKBO", 27 | "TKOffset", 28 | "TKXSBase", 29 | "TKCAF", 30 | "TKLCAF", 31 | "TKXCAF", 32 | ]; 33 | 34 | fn main() { 35 | let target = std::env::var("TARGET").expect("No TARGET environment variable defined"); 36 | let is_windows = target.to_lowercase().contains("windows"); 37 | let is_windows_gnu = target.to_lowercase().contains("windows-gnu"); 38 | 39 | let occt_config = OcctConfig::detect(); 40 | 41 | println!("cargo:rustc-link-search=native={}", occt_config.library_dir.to_str().unwrap()); 42 | 43 | let lib_type = if occt_config.is_dynamic { "dylib" } else { "static" }; 44 | for lib in OCCT_LIBS { 45 | println!("cargo:rustc-link-lib={lib_type}={lib}"); 46 | } 47 | 48 | if is_windows { 49 | println!("cargo:rustc-link-lib=dylib=user32"); 50 | } 51 | 52 | let mut build = cxx_build::bridge("src/lib.rs"); 53 | 54 | if is_windows_gnu { 55 | build.define("OCC_CONVERT_SIGNALS", "TRUE"); 56 | } 57 | 58 | if let "windows" = std::env::consts::OS { 59 | let current = std::env::current_dir().unwrap(); 60 | build.include(current.parent().unwrap()); 61 | } 62 | 63 | build 64 | .cpp(true) 65 | .flag_if_supported("-std=c++11") 66 | .define("_USE_MATH_DEFINES", "TRUE") 67 | .include(occt_config.include_dir) 68 | .include("include") 69 | .compile("wrapper"); 70 | 71 | println!("cargo:rustc-link-lib=static=wrapper"); 72 | 73 | println!("cargo:rerun-if-changed=src/lib.rs"); 74 | println!("cargo:rerun-if-changed=include/wrapper.hxx"); 75 | } 76 | 77 | struct OcctConfig { 78 | include_dir: std::path::PathBuf, 79 | library_dir: std::path::PathBuf, 80 | is_dynamic: bool, 81 | } 82 | 83 | impl OcctConfig { 84 | /// Find OpenCASCADE library using cmake 85 | fn detect() -> Self { 86 | println!("cargo:rerun-if-env-changed=DEP_OCCT_ROOT"); 87 | 88 | // Add path to builtin OCCT 89 | #[cfg(feature = "builtin")] 90 | { 91 | occt_sys::build_occt(); 92 | std::env::set_var("DEP_OCCT_ROOT", occt_sys::occt_path().as_os_str()); 93 | } 94 | 95 | let dst = 96 | std::panic::catch_unwind(|| cmake::Config::new("OCCT").register_dep("occt").build()); 97 | 98 | #[cfg(feature = "builtin")] 99 | let dst = dst.expect("Builtin OpenCASCADE library not found."); 100 | 101 | #[cfg(not(feature = "builtin"))] 102 | let dst = dst.expect("Pre-installed OpenCASCADE library not found. You can use `builtin` feature if you do not want to install OCCT libraries system-wide."); 103 | 104 | let cfg = std::fs::read_to_string(dst.join("share").join("occ_info.txt")) 105 | .expect("Something went wrong when detecting OpenCASCADE library."); 106 | 107 | let mut version_major: Option = None; 108 | let mut version_minor: Option = None; 109 | let mut include_dir: Option = None; 110 | let mut library_dir: Option = None; 111 | let mut is_dynamic: bool = false; 112 | 113 | for line in cfg.lines() { 114 | if let Some((var, val)) = line.split_once('=') { 115 | match var { 116 | "VERSION_MAJOR" => version_major = val.parse().ok(), 117 | "VERSION_MINOR" => version_minor = val.parse().ok(), 118 | "INCLUDE_DIR" => include_dir = val.parse().ok(), 119 | "LIBRARY_DIR" => library_dir = val.parse().ok(), 120 | "BUILD_SHARED_LIBS" => is_dynamic = val == "ON", 121 | _ => (), 122 | } 123 | } 124 | } 125 | 126 | if let (Some(version_major), Some(version_minor), Some(include_dir), Some(library_dir)) = 127 | (version_major, version_minor, include_dir, library_dir) 128 | { 129 | if version_major != OCCT_VERSION.0 || version_minor < OCCT_VERSION.1 { 130 | #[cfg(feature = "builtin")] 131 | panic!("Builtin OpenCASCADE library found but version is not met (found {}.{} but {}.{} required). Please fix OCCT_VERSION in build script of `opencascade-sys` crate or submodule OCCT in `occt-sys` crate.", 132 | version_major, version_minor, OCCT_VERSION.0, OCCT_VERSION.1); 133 | 134 | #[cfg(not(feature = "builtin"))] 135 | panic!("Pre-installed OpenCASCADE library found but version is not met (found {}.{} but {}.{} required). Please provide required version or use `builtin` feature.", 136 | version_major, version_minor, OCCT_VERSION.0, OCCT_VERSION.1); 137 | } 138 | 139 | Self { include_dir, library_dir, is_dynamic } 140 | } else { 141 | panic!("OpenCASCADE library found but something wrong with config."); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/opencascade-sys/examples/bottle.rs: -------------------------------------------------------------------------------- 1 | use cxx::UniquePtr; 2 | use opencascade_sys::ffi::{ 3 | cylinder_to_surface, ellipse_to_HandleGeom2d_Curve, ellipse_value, gp_Ax2_ctor, gp_Ax2d_ctor, 4 | gp_Ax3_from_gp_Ax2, gp_DZ, gp_Dir2d_ctor, gp_OX, handle_geom_plane_location, 5 | new_HandleGeomCurve_from_HandleGeom_TrimmedCurve, new_HandleGeomPlane_from_HandleGeomSurface, 6 | new_list_of_shape, new_point, new_point_2d, new_transform, new_vec, shape_list_append_face, 7 | type_name, write_stl, BRepAlgoAPI_Fuse_ctor, BRepBuilderAPI_MakeEdge_CurveSurface2d, 8 | BRepBuilderAPI_MakeEdge_HandleGeomCurve, BRepBuilderAPI_MakeFace_wire, 9 | BRepBuilderAPI_MakeWire_ctor, BRepBuilderAPI_MakeWire_edge_edge, 10 | BRepBuilderAPI_MakeWire_edge_edge_edge, BRepBuilderAPI_Transform_ctor, 11 | BRepFilletAPI_MakeFillet_ctor, BRepLibBuildCurves3d, BRepMesh_IncrementalMesh_ctor, 12 | BRepOffsetAPI_MakeThickSolid_ctor, BRepOffsetAPI_ThruSections_ctor, 13 | BRepPrimAPI_MakeCylinder_ctor, BRepPrimAPI_MakePrism_ctor, BRep_Builder_ctor, 14 | BRep_Builder_upcast_to_topods_builder, BRep_Tool_Surface, DynamicType, ExplorerCurrentShape, 15 | GCE2d_MakeSegment_point_point, GC_MakeArcOfCircle_Value, GC_MakeArcOfCircle_point_point_point, 16 | GC_MakeSegment_Value, GC_MakeSegment_point_point, Geom2d_Ellipse_ctor, 17 | Geom2d_TrimmedCurve_ctor, Geom_CylindricalSurface_ctor, HandleGeom2d_TrimmedCurve_to_curve, 18 | MakeThickSolidByJoin, StlAPI_Writer_ctor, TopAbs_ShapeEnum, TopExp_Explorer_ctor, 19 | TopoDS_Compound_as_shape, TopoDS_Compound_ctor, TopoDS_Face, TopoDS_Face_to_owned, 20 | TopoDS_cast_to_edge, TopoDS_cast_to_face, TopoDS_cast_to_wire, 21 | }; 22 | 23 | // All dimensions are in millimeters. 24 | pub fn main() { 25 | let height = 70.0; 26 | let width = 50.0; 27 | let thickness = 30.0; 28 | 29 | // Define the points making up the bottle's profile. 30 | let point_1 = new_point(-width / 2.0, 0.0, 0.0); 31 | let point_2 = new_point(-width / 2.0, -thickness / 4.0, 0.0); 32 | let point_3 = new_point(0.0, -thickness / 2.0, 0.0); 33 | let point_4 = new_point(width / 2.0, -thickness / 4.0, 0.0); 34 | let point_5 = new_point(width / 2.0, 0.0, 0.0); 35 | 36 | // Define the arcs and segments of the profile. 37 | let arc = GC_MakeArcOfCircle_point_point_point(&point_2, &point_3, &point_4); 38 | let segment_1 = GC_MakeSegment_point_point(&point_1, &point_2); 39 | let segment_2 = GC_MakeSegment_point_point(&point_4, &point_5); 40 | 41 | let mut edge_1 = BRepBuilderAPI_MakeEdge_HandleGeomCurve( 42 | &new_HandleGeomCurve_from_HandleGeom_TrimmedCurve(&GC_MakeSegment_Value(&segment_1)), 43 | ); 44 | 45 | let mut edge_2 = BRepBuilderAPI_MakeEdge_HandleGeomCurve( 46 | &new_HandleGeomCurve_from_HandleGeom_TrimmedCurve(&GC_MakeArcOfCircle_Value(&arc)), 47 | ); 48 | 49 | let mut edge_3 = BRepBuilderAPI_MakeEdge_HandleGeomCurve( 50 | &new_HandleGeomCurve_from_HandleGeom_TrimmedCurve(&GC_MakeSegment_Value(&segment_2)), 51 | ); 52 | 53 | let mut wire = BRepBuilderAPI_MakeWire_edge_edge_edge( 54 | edge_1.pin_mut().Edge(), 55 | edge_2.pin_mut().Edge(), 56 | edge_3.pin_mut().Edge(), 57 | ); 58 | 59 | let x_axis = gp_OX(); 60 | 61 | let mut transform = new_transform(); 62 | transform.pin_mut().set_mirror_axis(x_axis); 63 | 64 | // We're calling Shape() here instead of Wire(), hope that's okay. 65 | let mut brep_transform = 66 | BRepBuilderAPI_Transform_ctor(wire.pin_mut().Shape(), &transform, false); 67 | let mirrored_shape = brep_transform.pin_mut().Shape(); 68 | let mirrored_wire = TopoDS_cast_to_wire(mirrored_shape); 69 | 70 | let mut make_wire = BRepBuilderAPI_MakeWire_ctor(); 71 | make_wire.pin_mut().add_wire(wire.pin_mut().Wire()); 72 | make_wire.pin_mut().add_wire(mirrored_wire); 73 | 74 | let wire_profile = make_wire.pin_mut().Wire(); 75 | 76 | let mut face_profile = BRepBuilderAPI_MakeFace_wire(wire_profile, false); 77 | let prism_vec = new_vec(0.0, 0.0, height); 78 | // We're calling Shape here instead of Face(), hope that's also okay. 79 | let mut body = 80 | BRepPrimAPI_MakePrism_ctor(face_profile.pin_mut().Shape(), &prism_vec, false, true); 81 | 82 | let mut make_fillet = BRepFilletAPI_MakeFillet_ctor(body.pin_mut().Shape()); 83 | let mut edge_explorer = 84 | TopExp_Explorer_ctor(body.pin_mut().Shape(), TopAbs_ShapeEnum::TopAbs_EDGE); 85 | 86 | while edge_explorer.More() { 87 | let edge = TopoDS_cast_to_edge(edge_explorer.Current()); 88 | make_fillet.pin_mut().add_edge(thickness / 12.0, edge); 89 | edge_explorer.pin_mut().Next(); 90 | } 91 | 92 | let body_shape = make_fillet.pin_mut().Shape(); 93 | 94 | // Make the bottle neck 95 | let neck_location = new_point(0.0, 0.0, height); 96 | let neck_axis = gp_DZ(); 97 | let neck_coord_system = gp_Ax2_ctor(&neck_location, neck_axis); 98 | 99 | let neck_radius = thickness / 4.0; 100 | let neck_height = height / 10.0; 101 | 102 | let mut cylinder = BRepPrimAPI_MakeCylinder_ctor(&neck_coord_system, neck_radius, neck_height); 103 | let cylinder_shape = cylinder.pin_mut().Shape(); 104 | 105 | let mut fuse_neck = BRepAlgoAPI_Fuse_ctor(body_shape, cylinder_shape); 106 | let body_shape = fuse_neck.pin_mut().Shape(); 107 | 108 | // Make the bottle hollow 109 | let mut face_explorer = TopExp_Explorer_ctor(body_shape, TopAbs_ShapeEnum::TopAbs_FACE); 110 | let mut z_max = -1.0; 111 | let mut top_face: Option> = None; 112 | 113 | while face_explorer.More() { 114 | let shape = ExplorerCurrentShape(&face_explorer); 115 | let face = TopoDS_cast_to_face(&shape); 116 | 117 | let surface = BRep_Tool_Surface(face); 118 | let dynamic_type = DynamicType(&surface); 119 | let name = type_name(dynamic_type); 120 | 121 | if name == "Geom_Plane" { 122 | let plane_handle = new_HandleGeomPlane_from_HandleGeomSurface(&surface); 123 | let plane_location = handle_geom_plane_location(&plane_handle); 124 | 125 | let plane_z = plane_location.Z(); 126 | if plane_z > z_max { 127 | z_max = plane_z; 128 | top_face = Some(TopoDS_Face_to_owned(face)); 129 | } 130 | } 131 | 132 | face_explorer.pin_mut().Next(); 133 | } 134 | 135 | let top_face = top_face.unwrap(); 136 | 137 | let mut faces_to_remove = new_list_of_shape(); 138 | shape_list_append_face(faces_to_remove.pin_mut(), &top_face); 139 | 140 | let mut solid_maker = BRepOffsetAPI_MakeThickSolid_ctor(); 141 | MakeThickSolidByJoin( 142 | solid_maker.pin_mut(), 143 | body_shape, 144 | &faces_to_remove, 145 | -thickness / 50.0, 146 | 1.0e-3, 147 | ); 148 | 149 | let body_shape = solid_maker.pin_mut().Shape(); 150 | 151 | // Create the threading 152 | let cylinder_axis = gp_Ax3_from_gp_Ax2(&neck_coord_system); 153 | let cylinder_1 = Geom_CylindricalSurface_ctor(&cylinder_axis, neck_radius * 0.99); 154 | let cylinder_1 = cylinder_to_surface(&cylinder_1); 155 | let cylinder_2 = Geom_CylindricalSurface_ctor(&cylinder_axis, neck_radius * 1.05); 156 | let cylinder_2 = cylinder_to_surface(&cylinder_2); 157 | 158 | let a_pnt = new_point_2d(std::f64::consts::TAU, neck_height / 2.0); 159 | let a_dir = gp_Dir2d_ctor(std::f64::consts::TAU, neck_height / 4.0); 160 | let thread_axis = gp_Ax2d_ctor(&a_pnt, &a_dir); 161 | 162 | let a_major = std::f64::consts::TAU; 163 | let a_minor = neck_height / 10.0; 164 | 165 | let ellipse_1 = Geom2d_Ellipse_ctor(&thread_axis, a_major, a_minor); 166 | let ellipse_1_handle = ellipse_to_HandleGeom2d_Curve(&ellipse_1); 167 | let ellipse_2 = Geom2d_Ellipse_ctor(&thread_axis, a_major, a_minor / 4.0); 168 | let ellipse_2_handle = ellipse_to_HandleGeom2d_Curve(&ellipse_2); 169 | let arc_1 = Geom2d_TrimmedCurve_ctor(&ellipse_1_handle, 0.0, std::f64::consts::PI); 170 | let arc_1 = HandleGeom2d_TrimmedCurve_to_curve(&arc_1); 171 | let arc_2 = Geom2d_TrimmedCurve_ctor(&ellipse_2_handle, 0.0, std::f64::consts::PI); 172 | let arc_2 = HandleGeom2d_TrimmedCurve_to_curve(&arc_2); 173 | 174 | let ellipse_point_1 = ellipse_value(&ellipse_1, 0.0); 175 | let ellipse_point_2 = ellipse_value(&ellipse_1, std::f64::consts::PI); 176 | let thread_segment = GCE2d_MakeSegment_point_point(&ellipse_point_1, &ellipse_point_2); 177 | let thread_segment = HandleGeom2d_TrimmedCurve_to_curve(&thread_segment); 178 | 179 | let mut edge_1_on_surface_1 = BRepBuilderAPI_MakeEdge_CurveSurface2d(&arc_1, &cylinder_1); 180 | let mut edge_2_on_surface_1 = 181 | BRepBuilderAPI_MakeEdge_CurveSurface2d(&thread_segment, &cylinder_1); 182 | let mut edge_1_on_surface_2 = BRepBuilderAPI_MakeEdge_CurveSurface2d(&arc_2, &cylinder_2); 183 | let mut edge_2_on_surface_2 = 184 | BRepBuilderAPI_MakeEdge_CurveSurface2d(&thread_segment, &cylinder_2); 185 | 186 | let mut threading_wire_1 = BRepBuilderAPI_MakeWire_edge_edge( 187 | edge_1_on_surface_1.pin_mut().Edge(), 188 | edge_2_on_surface_1.pin_mut().Edge(), 189 | ); 190 | let mut threading_wire_2 = BRepBuilderAPI_MakeWire_edge_edge( 191 | edge_1_on_surface_2.pin_mut().Edge(), 192 | edge_2_on_surface_2.pin_mut().Edge(), 193 | ); 194 | 195 | // TODO - does calling Shape() work here instead of Wire()? 196 | BRepLibBuildCurves3d(threading_wire_1.pin_mut().Shape()); 197 | BRepLibBuildCurves3d(threading_wire_2.pin_mut().Shape()); 198 | 199 | let is_solid = true; 200 | let mut threading_loft = BRepOffsetAPI_ThruSections_ctor(is_solid); 201 | threading_loft.pin_mut().AddWire(threading_wire_1.pin_mut().Wire()); 202 | threading_loft.pin_mut().AddWire(threading_wire_2.pin_mut().Wire()); 203 | threading_loft.pin_mut().CheckCompatibility(false); 204 | 205 | let threading_shape = threading_loft.pin_mut().Shape(); 206 | 207 | // Build the resulting compound 208 | let mut compound = TopoDS_Compound_ctor(); 209 | let builder = BRep_Builder_ctor(); 210 | let builder = BRep_Builder_upcast_to_topods_builder(&builder); 211 | builder.MakeCompound(compound.pin_mut()); 212 | 213 | let mut compound_shape = TopoDS_Compound_as_shape(compound); 214 | builder.Add(compound_shape.pin_mut(), body_shape); 215 | builder.Add(compound_shape.pin_mut(), threading_shape); 216 | 217 | let final_shape = compound_shape; 218 | 219 | // Export to an STL file 220 | let mut stl_writer = StlAPI_Writer_ctor(); 221 | let triangulation = BRepMesh_IncrementalMesh_ctor(&final_shape, 0.01); 222 | let success = write_stl(stl_writer.pin_mut(), triangulation.Shape(), "bottle.stl".to_owned()); 223 | 224 | println!("Done! Success = {success}"); 225 | } 226 | -------------------------------------------------------------------------------- /crates/opencascade-sys/examples/simple.rs: -------------------------------------------------------------------------------- 1 | pub fn main() { 2 | let point = opencascade_sys::ffi::new_point(10.0, 7.0, 23.5); 3 | let y = point.Y(); 4 | println!("The point's Y value is {y}"); 5 | } 6 | -------------------------------------------------------------------------------- /crates/opencascade-sys/tests/triangulation.rs: -------------------------------------------------------------------------------- 1 | use opencascade_sys::ffi::{ 2 | new_point, BRepMesh_IncrementalMesh_ctor, BRepPrimAPI_MakeBox_ctor, BRep_Tool_Triangulation, 3 | HandlePoly_Triangulation_Get, Poly_Triangulation_Node, TopAbs_ShapeEnum, TopExp_Explorer_ctor, 4 | TopLoc_Location_ctor, TopoDS_cast_to_face, 5 | }; 6 | 7 | #[test] 8 | fn it_can_access_mesh_triangulation() { 9 | let origin = new_point(0., 0., 0.); 10 | let mut cube = BRepPrimAPI_MakeBox_ctor(&origin, 10., 10., 10.); 11 | 12 | let mut mesh = BRepMesh_IncrementalMesh_ctor(cube.pin_mut().Shape(), 0.01); 13 | 14 | let mut triangle_corners = 0; 15 | 16 | let mut edge_explorer = 17 | TopExp_Explorer_ctor(mesh.pin_mut().Shape(), TopAbs_ShapeEnum::TopAbs_FACE); 18 | while edge_explorer.More() { 19 | let face = TopoDS_cast_to_face(edge_explorer.Current()); 20 | let mut location = TopLoc_Location_ctor(); 21 | 22 | let triangulation_handle = BRep_Tool_Triangulation(face, location.pin_mut()); 23 | if let Ok(triangulation) = HandlePoly_Triangulation_Get(&triangulation_handle) { 24 | for index in 0..triangulation.NbTriangles() { 25 | let triangle = triangulation.Triangle(index + 1); 26 | 27 | for corner_index in 1..=3 { 28 | let _point = 29 | Poly_Triangulation_Node(triangulation, triangle.Value(corner_index)); 30 | triangle_corners += 1; 31 | } 32 | } 33 | } 34 | 35 | edge_explorer.pin_mut().Next(); 36 | } 37 | 38 | const SIDES: i32 = 6; 39 | const TRI_PER_SIDE: i32 = 2; 40 | const CORNER_PER_TRI: i32 = 3; 41 | assert_eq!(SIDES * TRI_PER_SIDE * CORNER_PER_TRI, triangle_corners) 42 | } 43 | -------------------------------------------------------------------------------- /crates/opencascade/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "opencascade" 3 | description = "A high level Rust wrapper to build 3D models in code, using the OpenCascade CAD kernel" 4 | authors = ["Brian Schwind "] 5 | license = "LGPL-2.1" 6 | version = "0.2.0" 7 | edition = "2021" 8 | repository = "https://github.com/bschwind/opencascade-rs" 9 | 10 | [dependencies] 11 | cxx = "1" 12 | opencascade-sys = { version = "0.2", path = "../opencascade-sys" } 13 | glam = { version = "0.24", features = ["bytemuck"] } 14 | kicad-parser = { path = "../kicad-parser" } 15 | thiserror = "1" 16 | 17 | [features] 18 | default = ["builtin"] 19 | builtin = ["opencascade-sys/builtin"] 20 | -------------------------------------------------------------------------------- /crates/opencascade/src/angle.rs: -------------------------------------------------------------------------------- 1 | use glam::{dvec3, DVec3}; 2 | use std::ops::{Div, Mul}; 3 | 4 | #[derive(Debug, Copy, Clone)] 5 | pub enum Angle { 6 | Radians(f64), 7 | Degrees(f64), 8 | } 9 | 10 | impl Angle { 11 | pub fn radians(self) -> f64 { 12 | match self { 13 | Self::Radians(r) => r, 14 | Self::Degrees(d) => (d * std::f64::consts::PI) / 180.0, 15 | } 16 | } 17 | 18 | pub fn degrees(self) -> f64 { 19 | match self { 20 | Self::Radians(r) => (r * 180.0) / std::f64::consts::PI, 21 | Self::Degrees(d) => d, 22 | } 23 | } 24 | } 25 | 26 | impl Mul for Angle { 27 | type Output = Angle; 28 | 29 | fn mul(self, multiplier: f64) -> Self::Output { 30 | match self { 31 | Self::Radians(angle) => Self::Radians(angle * multiplier), 32 | Self::Degrees(angle) => Self::Degrees(angle * multiplier), 33 | } 34 | } 35 | } 36 | 37 | impl Div for Angle { 38 | type Output = Angle; 39 | 40 | fn div(self, divisor: f64) -> Self::Output { 41 | match self { 42 | Self::Radians(angle) => Self::Radians(angle / divisor), 43 | Self::Degrees(angle) => Self::Degrees(angle / divisor), 44 | } 45 | } 46 | } 47 | 48 | pub trait ToAngle { 49 | fn degrees(&self) -> Angle; 50 | fn radians(&self) -> Angle; 51 | } 52 | 53 | impl + Copy> ToAngle for T { 54 | fn degrees(&self) -> Angle { 55 | Angle::Degrees((*self).into()) 56 | } 57 | 58 | fn radians(&self) -> Angle { 59 | Angle::Radians((*self).into()) 60 | } 61 | } 62 | 63 | /// Represents rotation on the X, Y, and Z axes. Also known 64 | /// as Euler angle representation. 65 | #[derive(Debug, Copy, Clone)] 66 | pub struct RVec { 67 | pub x: Angle, 68 | pub y: Angle, 69 | pub z: Angle, 70 | } 71 | 72 | impl RVec { 73 | pub fn radians(&self) -> DVec3 { 74 | dvec3(self.x.radians(), self.y.radians(), self.z.radians()) 75 | } 76 | 77 | pub fn degrees(&self) -> DVec3 { 78 | dvec3(self.x.degrees(), self.y.degrees(), self.z.degrees()) 79 | } 80 | 81 | pub fn x(x: Angle) -> Self { 82 | RVec { x, y: 0.degrees(), z: 0.degrees() } 83 | } 84 | 85 | pub fn y(y: Angle) -> Self { 86 | RVec { x: 0.degrees(), y, z: 0.degrees() } 87 | } 88 | 89 | pub fn z(z: Angle) -> Self { 90 | RVec { x: 0.degrees(), y: 0.degrees(), z } 91 | } 92 | } 93 | 94 | pub fn rvec(x: Angle, y: Angle, z: Angle) -> RVec { 95 | RVec { x, y, z } 96 | } 97 | -------------------------------------------------------------------------------- /crates/opencascade/src/kicad.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | angle::ToAngle, 3 | primitives::{Edge, EdgeConnection, Face, Wire}, 4 | workplane::Workplane, 5 | Error, 6 | }; 7 | use glam::DVec2; 8 | use kicad_parser::{ 9 | board::{BoardLayer, KicadBoard}, 10 | graphics::{GraphicArc, GraphicCircle, GraphicLine, GraphicRect}, 11 | }; 12 | use std::path::Path; 13 | 14 | impl From<&GraphicLine> for Edge { 15 | fn from(line: &GraphicLine) -> Edge { 16 | let start = DVec2::from(line.start_point); 17 | let end = DVec2::from(line.end_point); 18 | Edge::segment(start.extend(0.0), end.extend(0.0)) 19 | } 20 | } 21 | 22 | impl From<&GraphicArc> for Edge { 23 | fn from(arc: &GraphicArc) -> Edge { 24 | let start = DVec2::from(arc.start_point); 25 | let mid = DVec2::from(arc.mid_point); 26 | let end = DVec2::from(arc.end_point); 27 | Edge::arc(start.extend(0.0), mid.extend(0.0), end.extend(0.0)) 28 | } 29 | } 30 | 31 | impl From<&GraphicCircle> for Face { 32 | fn from(circle: &GraphicCircle) -> Face { 33 | let center = DVec2::from(circle.center_point); 34 | let end = DVec2::from(circle.end_point); 35 | 36 | let delta = (center - end).abs(); 37 | 38 | let radius = (delta.x * delta.x + delta.y * delta.y).sqrt(); 39 | Workplane::xy().translated(center.extend(0.0)).circle(center.x, center.y, radius).to_face() 40 | } 41 | } 42 | 43 | impl From<&GraphicRect> for Face { 44 | fn from(rect: &GraphicRect) -> Face { 45 | let start = DVec2::from(rect.start_point); 46 | let end = DVec2::from(rect.end_point); 47 | 48 | let dimensions = (end - start).abs(); 49 | Workplane::xy().translated(start.extend(0.0)).rect(dimensions.x, dimensions.y).to_face() 50 | } 51 | } 52 | 53 | pub struct KicadPcb { 54 | board: KicadBoard, 55 | } 56 | 57 | impl KicadPcb { 58 | pub fn from_file>(file: P) -> Result { 59 | Ok(Self { board: KicadBoard::from_file(file)? }) 60 | } 61 | 62 | pub fn edge_cuts(&self) -> Wire { 63 | Wire::from_unordered_edges( 64 | self.layer_edges(&BoardLayer::EdgeCuts), 65 | EdgeConnection::default(), 66 | ) 67 | } 68 | 69 | pub fn layer_edges<'a>(&'a self, layer: &'a BoardLayer) -> impl Iterator + 'a { 70 | let footprint_edges = self.board.footprints().flat_map(|footprint| { 71 | let angle = footprint.rotation_degrees.degrees(); 72 | // TODO(bschwind) - Document why a negative angle is needed here. 73 | let angle_vec = DVec2::from_angle(-angle.radians()); 74 | let translate = DVec2::from(footprint.location); 75 | 76 | footprint 77 | .lines() 78 | .filter(|line| line.layer == *layer) 79 | .map(move |line| { 80 | let start = line.start_point; 81 | let end = line.end_point; 82 | let start = DVec2::from(start); 83 | let end = DVec2::from(end); 84 | 85 | let start = translate + angle_vec.rotate(start); 86 | let end = translate + angle_vec.rotate(end); 87 | 88 | Edge::segment(start.extend(0.0), end.extend(0.0)) 89 | }) 90 | .chain(footprint.arcs().filter(|arc| arc.layer == *layer).map(move |arc| { 91 | let start = arc.start_point; 92 | let mid = arc.mid_point; 93 | let end = arc.end_point; 94 | let start = DVec2::from(start); 95 | let mid = DVec2::from(mid); 96 | let end = DVec2::from(end); 97 | 98 | let start = translate + angle_vec.rotate(start); 99 | let mid = translate + angle_vec.rotate(mid); 100 | let end = translate + angle_vec.rotate(end); 101 | 102 | Edge::arc(start.extend(0.0), mid.extend(0.0), end.extend(0.0)) 103 | })) 104 | }); 105 | 106 | self.board 107 | .lines() 108 | .filter(|line| line.layer == *layer) 109 | .map(Edge::from) 110 | .chain(self.board.arcs().filter(|arc| arc.layer == *layer).map(Edge::from)) 111 | .chain(footprint_edges) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /crates/opencascade/src/law_function.rs: -------------------------------------------------------------------------------- 1 | use cxx::UniquePtr; 2 | use glam::dvec2; 3 | use opencascade_sys::ffi; 4 | 5 | use crate::primitives::make_point2d; 6 | 7 | #[must_use] 8 | pub(crate) fn law_function_from_graph( 9 | pairs: impl IntoIterator, 10 | ) -> UniquePtr { 11 | let pairs: Vec<_> = pairs.into_iter().collect(); 12 | let mut array = ffi::TColgp_Array1OfPnt2d_ctor(1, pairs.len() as i32); 13 | 14 | for (index, (input, output)) in pairs.into_iter().enumerate() { 15 | array.pin_mut().SetValue(index as i32 + 1, &make_point2d(dvec2(input, output))); 16 | } 17 | 18 | let mut interpol = ffi::Law_Interpol_ctor(); 19 | let is_periodic = false; 20 | interpol.pin_mut().Set(&array, is_periodic); 21 | ffi::Law_Interpol_into_Law_Function(interpol) 22 | } 23 | -------------------------------------------------------------------------------- /crates/opencascade/src/lib.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub mod angle; 4 | pub mod kicad; 5 | pub mod mesh; 6 | pub mod primitives; 7 | pub mod section; 8 | pub mod workplane; 9 | 10 | mod law_function; 11 | mod make_pipe_shell; 12 | 13 | #[derive(Error, Debug)] 14 | pub enum Error { 15 | #[error("failed to write STL file")] 16 | StlWriteFailed, 17 | #[error("failed to read STEP file")] 18 | StepReadFailed, 19 | #[error("failed to read IGES file")] 20 | IgesReadFailed, 21 | #[error("failed to read KiCAD PCB file: {0}")] 22 | KicadReadFailed(#[from] kicad_parser::Error), 23 | #[error("failed to write STEP file")] 24 | StepWriteFailed, 25 | #[error("failed to write IGES file")] 26 | IgesWriteFailed, 27 | #[error("failed to triangulate Shape")] 28 | TriangulationFailed, 29 | #[error("encountered a face with no triangulation")] 30 | UntriangulatedFace, 31 | #[error("at least 2 points are required for creating a wire")] 32 | NotEnoughPoints, 33 | } 34 | -------------------------------------------------------------------------------- /crates/opencascade/src/make_pipe_shell.rs: -------------------------------------------------------------------------------- 1 | use cxx::UniquePtr; 2 | use opencascade_sys::ffi; 3 | 4 | #[must_use] 5 | pub(crate) fn make_pipe_shell_with_law_function( 6 | profile: &ffi::TopoDS_Wire, 7 | spine: &ffi::TopoDS_Wire, 8 | law_function: &ffi::HandleLawFunction, 9 | ) -> UniquePtr { 10 | let mut make_pipe_shell = ffi::BRepOffsetAPI_MakePipeShell_ctor(spine); 11 | make_pipe_shell.pin_mut().SetMode(false); 12 | let profile_shape = ffi::cast_wire_to_shape(profile); 13 | let with_contact = false; 14 | let with_correction = true; 15 | make_pipe_shell.pin_mut().SetLaw(profile_shape, law_function, with_contact, with_correction); 16 | make_pipe_shell 17 | } 18 | -------------------------------------------------------------------------------- /crates/opencascade/src/mesh.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | primitives::{FaceOrientation, Shape}, 3 | Error, 4 | }; 5 | use cxx::UniquePtr; 6 | use glam::{dvec2, dvec3, DVec2, DVec3}; 7 | use opencascade_sys::ffi; 8 | 9 | #[derive(Debug)] 10 | pub struct Mesh { 11 | pub vertices: Vec, 12 | pub uvs: Vec, 13 | pub normals: Vec, 14 | pub indices: Vec, 15 | } 16 | 17 | pub struct Mesher { 18 | pub(crate) inner: UniquePtr, 19 | } 20 | 21 | impl Mesher { 22 | pub fn try_new(shape: &Shape, triangulation_tolerance: f64) -> Result { 23 | let inner = ffi::BRepMesh_IncrementalMesh_ctor(&shape.inner, triangulation_tolerance); 24 | 25 | if inner.IsDone() { 26 | Ok(Self { inner }) 27 | } else { 28 | Err(Error::TriangulationFailed) 29 | } 30 | } 31 | 32 | pub fn mesh(mut self) -> Result { 33 | let mut vertices = vec![]; 34 | let mut uvs = vec![]; 35 | let mut normals = vec![]; 36 | let mut indices = vec![]; 37 | 38 | let triangulated_shape = Shape::from_shape(self.inner.pin_mut().Shape()); 39 | 40 | for face in triangulated_shape.faces() { 41 | let mut location = ffi::TopLoc_Location_ctor(); 42 | 43 | let triangulation_handle = 44 | ffi::BRep_Tool_Triangulation(&face.inner, location.pin_mut()); 45 | 46 | let triangulation = ffi::HandlePoly_Triangulation_Get(&triangulation_handle) 47 | .map_err(|_| Error::UntriangulatedFace)?; 48 | 49 | let index_offset = vertices.len(); 50 | let face_point_count = triangulation.NbNodes(); 51 | 52 | for i in 1..=face_point_count { 53 | let mut point = ffi::Poly_Triangulation_Node(triangulation, i); 54 | point.pin_mut().Transform(&ffi::TopLoc_Location_Transformation(&location)); 55 | vertices.push(dvec3(point.X(), point.Y(), point.Z())); 56 | } 57 | 58 | let mut u_min = f64::INFINITY; 59 | let mut v_min = f64::INFINITY; 60 | 61 | let mut u_max = f64::NEG_INFINITY; 62 | let mut v_max = f64::NEG_INFINITY; 63 | 64 | for i in 1..=(face_point_count) { 65 | let uv = ffi::Poly_Triangulation_UV(triangulation, i); 66 | let (u, v) = (uv.X(), uv.Y()); 67 | 68 | u_min = u_min.min(u); 69 | v_min = v_min.min(v); 70 | 71 | u_max = u_max.max(u); 72 | v_max = v_max.max(v); 73 | 74 | uvs.push(dvec2(u, v)); 75 | } 76 | 77 | // Normalize the newly added UV coordinates. 78 | for uv in &mut uvs[index_offset..(index_offset + face_point_count as usize)] { 79 | uv.x = (uv.x - u_min) / (u_max - u_min); 80 | uv.y = (uv.y - v_min) / (v_max - v_min); 81 | 82 | if face.orientation() != FaceOrientation::Forward { 83 | uv.x = 1.0 - uv.x; 84 | } 85 | } 86 | 87 | // Add in the normals. 88 | // TODO(bschwind) - Use `location` to transform the normals. 89 | let normal_array = ffi::TColgp_Array1OfDir_ctor(0, face_point_count); 90 | 91 | ffi::compute_normals(&face.inner, &triangulation_handle); 92 | 93 | // TODO(bschwind) - Why do we start at 1 here? 94 | for i in 1..(normal_array.Length() as usize) { 95 | let normal = ffi::Poly_Triangulation_Normal(triangulation, i as i32); 96 | normals.push(dvec3(normal.X(), normal.Y(), normal.Z())); 97 | } 98 | 99 | for i in 1..=triangulation.NbTriangles() { 100 | let triangle = triangulation.Triangle(i); 101 | 102 | if face.orientation() == FaceOrientation::Forward { 103 | indices.push(index_offset + triangle.Value(1) as usize - 1); 104 | indices.push(index_offset + triangle.Value(2) as usize - 1); 105 | indices.push(index_offset + triangle.Value(3) as usize - 1); 106 | } else { 107 | indices.push(index_offset + triangle.Value(3) as usize - 1); 108 | indices.push(index_offset + triangle.Value(2) as usize - 1); 109 | indices.push(index_offset + triangle.Value(1) as usize - 1); 110 | } 111 | } 112 | } 113 | 114 | Ok(Mesh { vertices, uvs, normals, indices }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives.rs: -------------------------------------------------------------------------------- 1 | use cxx::UniquePtr; 2 | use glam::{DVec2, DVec3}; 3 | use opencascade_sys::ffi; 4 | 5 | mod boolean_shape; 6 | mod compound; 7 | mod edge; 8 | mod face; 9 | mod shape; 10 | mod shell; 11 | mod solid; 12 | mod surface; 13 | mod vertex; 14 | mod wire; 15 | 16 | pub use boolean_shape::*; 17 | pub use compound::*; 18 | pub use edge::*; 19 | pub use face::*; 20 | pub use shape::*; 21 | pub use shell::*; 22 | pub use solid::*; 23 | pub use surface::*; 24 | pub use vertex::*; 25 | pub use wire::*; 26 | 27 | #[derive(Debug, Copy, Clone, PartialEq)] 28 | pub enum ShapeType { 29 | /// Abstract topological data structure describes a basic entity. 30 | Shape, 31 | 32 | /// A zero-dimensional shape corresponding to a point in geometry. 33 | Vertex, 34 | 35 | /// A single dimensional shape correspondingto a curve, and bound 36 | /// by a vertex at each extremity. 37 | Edge, 38 | 39 | /// A sequence of edges connected by their vertices. It can be open 40 | /// or closed depending on whether the edges are linked or not. 41 | Wire, 42 | 43 | /// Part of a plane (in 2D geometry) or a surface(in 3D geometry) 44 | /// bounded by a closed wire. Its geometry is constrained (trimmed) 45 | /// by contours. 46 | Face, 47 | 48 | /// A set of faces connected by some of the 49 | /// edges of their wire boundaries. A shell can be open or closed. 50 | Shell, 51 | 52 | /// A part of 3D space bounded by shells. 53 | Solid, 54 | 55 | /// A set of solids connected by their faces. This expands 56 | /// the notions of Wire and Shell to solids. 57 | CompoundSolid, 58 | 59 | /// A group of any of the shapes below. 60 | Compound, 61 | } 62 | 63 | impl From for ShapeType { 64 | fn from(shape_enum: ffi::TopAbs_ShapeEnum) -> Self { 65 | match shape_enum { 66 | ffi::TopAbs_ShapeEnum::TopAbs_SHAPE => ShapeType::Shape, 67 | ffi::TopAbs_ShapeEnum::TopAbs_VERTEX => ShapeType::Vertex, 68 | ffi::TopAbs_ShapeEnum::TopAbs_EDGE => ShapeType::Edge, 69 | ffi::TopAbs_ShapeEnum::TopAbs_WIRE => ShapeType::Wire, 70 | ffi::TopAbs_ShapeEnum::TopAbs_FACE => ShapeType::Face, 71 | ffi::TopAbs_ShapeEnum::TopAbs_SHELL => ShapeType::Shell, 72 | ffi::TopAbs_ShapeEnum::TopAbs_SOLID => ShapeType::Solid, 73 | ffi::TopAbs_ShapeEnum::TopAbs_COMPSOLID => ShapeType::CompoundSolid, 74 | ffi::TopAbs_ShapeEnum::TopAbs_COMPOUND => ShapeType::Compound, 75 | ffi::TopAbs_ShapeEnum { repr } => panic!("Unexpected shape type: {repr}"), 76 | } 77 | } 78 | } 79 | 80 | pub trait IntoShape { 81 | fn into_shape(self) -> Shape; 82 | } 83 | 84 | impl> IntoShape for T { 85 | fn into_shape(self) -> Shape { 86 | self.into() 87 | } 88 | } 89 | 90 | pub fn make_point(p: DVec3) -> UniquePtr { 91 | ffi::new_point(p.x, p.y, p.z) 92 | } 93 | 94 | pub fn make_point2d(p: DVec2) -> UniquePtr { 95 | ffi::new_point_2d(p.x, p.y) 96 | } 97 | 98 | fn make_dir(p: DVec3) -> UniquePtr { 99 | ffi::gp_Dir_ctor(p.x, p.y, p.z) 100 | } 101 | 102 | fn make_vec(vec: DVec3) -> UniquePtr { 103 | ffi::new_vec(vec.x, vec.y, vec.z) 104 | } 105 | 106 | fn make_axis_1(origin: DVec3, dir: DVec3) -> UniquePtr { 107 | ffi::gp_Ax1_ctor(&make_point(origin), &make_dir(dir)) 108 | } 109 | 110 | pub fn make_axis_2(origin: DVec3, dir: DVec3) -> UniquePtr { 111 | ffi::gp_Ax2_ctor(&make_point(origin), &make_dir(dir)) 112 | } 113 | 114 | pub struct EdgeIterator { 115 | explorer: UniquePtr, 116 | } 117 | 118 | impl Iterator for EdgeIterator { 119 | type Item = Edge; 120 | 121 | fn next(&mut self) -> Option { 122 | if self.explorer.More() { 123 | let edge = ffi::TopoDS_cast_to_edge(self.explorer.Current()); 124 | let edge = Edge::from_edge(edge); 125 | 126 | self.explorer.pin_mut().Next(); 127 | 128 | Some(edge) 129 | } else { 130 | None 131 | } 132 | } 133 | } 134 | 135 | impl EdgeIterator { 136 | pub fn parallel_to( 137 | self, 138 | direction: Direction, 139 | ) -> impl Iterator::Item> { 140 | let normalized_dir = direction.normalized_vec(); 141 | 142 | self.filter(move |edge| { 143 | edge.edge_type() == EdgeType::Line 144 | && 1.0 145 | - (edge.end_point() - edge.start_point()).normalize().dot(normalized_dir).abs() 146 | < 0.0001 147 | }) 148 | } 149 | } 150 | 151 | pub struct FaceIterator { 152 | explorer: UniquePtr, 153 | } 154 | 155 | #[derive(Debug, Copy, Clone)] 156 | pub enum Direction { 157 | PosX, 158 | NegX, 159 | PosY, 160 | NegY, 161 | PosZ, 162 | NegZ, 163 | Custom(DVec3), 164 | } 165 | 166 | impl Direction { 167 | pub fn normalized_vec(&self) -> DVec3 { 168 | match self { 169 | Self::PosX => DVec3::X, 170 | Self::NegX => DVec3::NEG_X, 171 | Self::PosY => DVec3::Y, 172 | Self::NegY => DVec3::NEG_Y, 173 | Self::PosZ => DVec3::Z, 174 | Self::NegZ => DVec3::NEG_Z, 175 | Self::Custom(dir) => dir.normalize(), 176 | } 177 | } 178 | } 179 | 180 | impl FaceIterator { 181 | pub fn farthest(self, direction: Direction) -> Face { 182 | self.try_farthest(direction).unwrap() 183 | } 184 | 185 | pub fn try_farthest(self, direction: Direction) -> Option { 186 | let normalized_dir = direction.normalized_vec(); 187 | 188 | self.max_by(|face_1, face_2| { 189 | let dist_1 = face_1.center_of_mass().dot(normalized_dir); 190 | let dist_2 = face_2.center_of_mass().dot(normalized_dir); 191 | 192 | PartialOrd::partial_cmp(&dist_1, &dist_2) 193 | .expect("Face center of masses should contain no NaNs") 194 | }) 195 | } 196 | } 197 | 198 | impl Iterator for FaceIterator { 199 | type Item = Face; 200 | 201 | fn next(&mut self) -> Option { 202 | if self.explorer.More() { 203 | let face = ffi::TopoDS_cast_to_face(self.explorer.Current()); 204 | let face = Face::from_face(face); 205 | 206 | self.explorer.pin_mut().Next(); 207 | 208 | Some(face) 209 | } else { 210 | None 211 | } 212 | } 213 | } 214 | 215 | /// Given n and func, returns an iterator of (t, f(t)) values 216 | /// where t is in the range [0, 1]. 217 | /// Note that n + 1 values are returned. 218 | pub fn approximate_function f64>( 219 | n: usize, 220 | mut func: F, 221 | ) -> impl Iterator { 222 | let mut count = 0; 223 | 224 | std::iter::from_fn(move || { 225 | if count > n { 226 | return None; 227 | } 228 | 229 | let t = count as f64 / n as f64; 230 | count += 1; 231 | 232 | let val = func(t); 233 | 234 | Some((t, val)) 235 | }) 236 | } 237 | 238 | #[derive(Debug, Copy, Clone, PartialEq)] 239 | pub enum JoinType { 240 | Arc, 241 | // TODO(mkovaxx): Figure out how to make tangent joints work without segfaults 242 | //Tangent, 243 | Intersection, 244 | } 245 | 246 | impl From for JoinType { 247 | fn from(value: ffi::GeomAbs_JoinType) -> Self { 248 | match value { 249 | ffi::GeomAbs_JoinType::GeomAbs_Arc => Self::Arc, 250 | //ffi::GeomAbs_JoinType::GeomAbs_Tangent => Self::Tangent, 251 | ffi::GeomAbs_JoinType::GeomAbs_Intersection => Self::Intersection, 252 | ffi::GeomAbs_JoinType { repr } => panic!("Unexpected join type: {repr}"), 253 | } 254 | } 255 | } 256 | 257 | impl From for ffi::GeomAbs_JoinType { 258 | fn from(value: JoinType) -> Self { 259 | match value { 260 | JoinType::Arc => ffi::GeomAbs_JoinType::GeomAbs_Arc, 261 | //JoinType::Tangent => ffi::GeomAbs_JoinType::GeomAbs_Tangent, 262 | JoinType::Intersection => ffi::GeomAbs_JoinType::GeomAbs_Intersection, 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives/boolean_shape.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::{Edge, Shape}; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | /// The result of running a boolean operation (union, subtraction, intersection) 5 | /// on two shapes. 6 | pub struct BooleanShape { 7 | pub shape: Shape, 8 | pub new_edges: Vec, 9 | } 10 | 11 | impl Deref for BooleanShape { 12 | type Target = Shape; 13 | 14 | fn deref(&self) -> &Self::Target { 15 | &self.shape 16 | } 17 | } 18 | 19 | impl DerefMut for BooleanShape { 20 | fn deref_mut(&mut self) -> &mut Self::Target { 21 | &mut self.shape 22 | } 23 | } 24 | 25 | impl BooleanShape { 26 | pub fn new_edges(&self) -> impl Iterator { 27 | self.new_edges.iter() 28 | } 29 | 30 | #[must_use] 31 | pub fn fillet_new_edges(&self, radius: f64) -> Shape { 32 | self.shape.fillet_edges(radius, &self.new_edges) 33 | } 34 | 35 | #[must_use] 36 | pub fn variable_fillet_new_edges( 37 | &self, 38 | radius_values: impl IntoIterator, 39 | ) -> Shape { 40 | self.shape.variable_fillet_edges(radius_values, &self.new_edges) 41 | } 42 | 43 | #[must_use] 44 | pub fn chamfer_new_edges(&self, distance: f64) -> Shape { 45 | self.shape.chamfer_edges(distance, &self.new_edges) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives/compound.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::Shape; 2 | use cxx::UniquePtr; 3 | use opencascade_sys::ffi; 4 | 5 | pub struct Compound { 6 | pub(crate) inner: UniquePtr, 7 | } 8 | 9 | impl AsRef for Compound { 10 | fn as_ref(&self) -> &Compound { 11 | self 12 | } 13 | } 14 | 15 | impl Compound { 16 | pub(crate) fn from_compound(compound: &ffi::TopoDS_Compound) -> Self { 17 | let inner = ffi::TopoDS_Compound_to_owned(compound); 18 | 19 | Self { inner } 20 | } 21 | 22 | #[must_use] 23 | pub fn clean(&self) -> Shape { 24 | let shape = ffi::cast_compound_to_shape(&self.inner); 25 | 26 | Shape::from_shape(shape).clean() 27 | } 28 | 29 | pub fn from_shapes>(shapes: impl IntoIterator) -> Self { 30 | let mut compound = ffi::TopoDS_Compound_ctor(); 31 | let builder = ffi::BRep_Builder_ctor(); 32 | let builder = ffi::BRep_Builder_upcast_to_topods_builder(&builder); 33 | builder.MakeCompound(compound.pin_mut()); 34 | let mut compound_shape = ffi::TopoDS_Compound_as_shape(compound); 35 | 36 | for shape in shapes.into_iter() { 37 | builder.Add(compound_shape.pin_mut(), &shape.as_ref().inner); 38 | } 39 | 40 | let compound = ffi::TopoDS_cast_to_compound(&compound_shape); 41 | Self::from_compound(compound) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives/edge.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::{make_axis_2, make_point}; 2 | use cxx::UniquePtr; 3 | use glam::{dvec3, DVec3}; 4 | use opencascade_sys::ffi; 5 | 6 | use super::make_vec; 7 | 8 | #[derive(Debug, Copy, Clone, PartialEq)] 9 | pub enum EdgeType { 10 | Line, 11 | Circle, 12 | Ellipse, 13 | Hyperbola, 14 | Parabola, 15 | BezierCurve, 16 | BSplineCurve, 17 | OffsetCurve, 18 | OtherCurve, 19 | } 20 | 21 | impl From for EdgeType { 22 | fn from(curve_type: ffi::GeomAbs_CurveType) -> Self { 23 | match curve_type { 24 | ffi::GeomAbs_CurveType::GeomAbs_Line => Self::Line, 25 | ffi::GeomAbs_CurveType::GeomAbs_Circle => Self::Circle, 26 | ffi::GeomAbs_CurveType::GeomAbs_Ellipse => Self::Ellipse, 27 | ffi::GeomAbs_CurveType::GeomAbs_Hyperbola => Self::Hyperbola, 28 | ffi::GeomAbs_CurveType::GeomAbs_Parabola => Self::Parabola, 29 | ffi::GeomAbs_CurveType::GeomAbs_BezierCurve => Self::BezierCurve, 30 | ffi::GeomAbs_CurveType::GeomAbs_BSplineCurve => Self::BSplineCurve, 31 | ffi::GeomAbs_CurveType::GeomAbs_OffsetCurve => Self::OffsetCurve, 32 | ffi::GeomAbs_CurveType::GeomAbs_OtherCurve => Self::OtherCurve, 33 | ffi::GeomAbs_CurveType { repr } => panic!("Unexpected curve type: {repr}"), 34 | } 35 | } 36 | } 37 | 38 | pub struct Edge { 39 | pub(crate) inner: UniquePtr, 40 | } 41 | 42 | impl AsRef for Edge { 43 | fn as_ref(&self) -> &Edge { 44 | self 45 | } 46 | } 47 | 48 | impl Edge { 49 | pub(crate) fn from_edge(edge: &ffi::TopoDS_Edge) -> Self { 50 | let inner = ffi::TopoDS_Edge_to_owned(edge); 51 | 52 | Self { inner } 53 | } 54 | 55 | fn from_make_edge(mut make_edge: UniquePtr) -> Self { 56 | Self::from_edge(make_edge.pin_mut().Edge()) 57 | } 58 | 59 | pub fn segment(p1: DVec3, p2: DVec3) -> Self { 60 | let make_edge = 61 | ffi::BRepBuilderAPI_MakeEdge_gp_Pnt_gp_Pnt(&make_point(p1), &make_point(p2)); 62 | 63 | Self::from_make_edge(make_edge) 64 | } 65 | 66 | pub fn bezier(points: impl IntoIterator) -> Self { 67 | let points: Vec<_> = points.into_iter().collect(); 68 | let mut array = ffi::TColgp_HArray1OfPnt_ctor(1, points.len() as i32); 69 | for (index, point) in points.into_iter().enumerate() { 70 | array.pin_mut().SetValue(index as i32 + 1, &make_point(point)); 71 | } 72 | 73 | let bezier = ffi::Geom_BezierCurve_ctor_points(&array); 74 | let bezier_handle = ffi::Geom_BezierCurve_to_handle(bezier); 75 | let curve_handle = ffi::new_HandleGeomCurve_from_HandleGeom_BezierCurve(&bezier_handle); 76 | 77 | let mut make_edge = ffi::BRepBuilderAPI_MakeEdge_HandleGeomCurve(&curve_handle); 78 | let edge = make_edge.pin_mut().Edge(); 79 | Self::from_edge(edge) 80 | } 81 | 82 | pub fn circle(center: DVec3, normal: DVec3, radius: f64) -> Self { 83 | let axis = make_axis_2(center, normal); 84 | 85 | let make_circle = ffi::gp_Circ_ctor(&axis, radius); 86 | let make_edge = ffi::BRepBuilderAPI_MakeEdge_circle(&make_circle); 87 | 88 | Self::from_make_edge(make_edge) 89 | } 90 | 91 | pub fn ellipse() {} 92 | 93 | pub fn spline_from_points( 94 | points: impl IntoIterator, 95 | tangents: Option<(DVec3, DVec3)>, 96 | ) -> Self { 97 | let points: Vec<_> = points.into_iter().collect(); 98 | let mut array = ffi::TColgp_HArray1OfPnt_ctor(1, points.len() as i32); 99 | for (index, point) in points.into_iter().enumerate() { 100 | array.pin_mut().SetValue(index as i32 + 1, &make_point(point)); 101 | } 102 | let array_handle = ffi::new_HandleTColgpHArray1OfPnt_from_TColgpHArray1OfPnt(array); 103 | 104 | let periodic = false; 105 | let tolerance = 1.0e-7; 106 | let mut interpolate = ffi::GeomAPI_Interpolate_ctor(&array_handle, periodic, tolerance); 107 | if let Some((t_start, t_end)) = tangents { 108 | interpolate.pin_mut().Load(&make_vec(t_start), &make_vec(t_end), true); 109 | } 110 | 111 | interpolate.pin_mut().Perform(); 112 | let bspline_handle = ffi::GeomAPI_Interpolate_Curve(&interpolate); 113 | let curve_handle = ffi::new_HandleGeomCurve_from_HandleGeom_BSplineCurve(&bspline_handle); 114 | 115 | let mut make_edge = ffi::BRepBuilderAPI_MakeEdge_HandleGeomCurve(&curve_handle); 116 | let edge = make_edge.pin_mut().Edge(); 117 | Self::from_edge(edge) 118 | } 119 | 120 | pub fn arc(p1: DVec3, p2: DVec3, p3: DVec3) -> Self { 121 | let make_arc = ffi::GC_MakeArcOfCircle_point_point_point( 122 | &make_point(p1), 123 | &make_point(p2), 124 | &make_point(p3), 125 | ); 126 | 127 | let make_edge = ffi::BRepBuilderAPI_MakeEdge_HandleGeomCurve( 128 | &ffi::new_HandleGeomCurve_from_HandleGeom_TrimmedCurve(&ffi::GC_MakeArcOfCircle_Value( 129 | &make_arc, 130 | )), 131 | ); 132 | 133 | Self::from_make_edge(make_edge) 134 | } 135 | 136 | pub fn start_point(&self) -> DVec3 { 137 | let curve = ffi::BRepAdaptor_Curve_ctor(&self.inner); 138 | let start_param = curve.FirstParameter(); 139 | let point = ffi::BRepAdaptor_Curve_value(&curve, start_param); 140 | 141 | dvec3(point.X(), point.Y(), point.Z()) 142 | } 143 | 144 | pub fn end_point(&self) -> DVec3 { 145 | let curve = ffi::BRepAdaptor_Curve_ctor(&self.inner); 146 | let last_param = curve.LastParameter(); 147 | let point = ffi::BRepAdaptor_Curve_value(&curve, last_param); 148 | 149 | dvec3(point.X(), point.Y(), point.Z()) 150 | } 151 | 152 | pub fn approximation_segments(&self) -> ApproximationSegmentIterator { 153 | let adaptor_curve = ffi::BRepAdaptor_Curve_ctor(&self.inner); 154 | let approximator = ffi::GCPnts_TangentialDeflection_ctor(&adaptor_curve, 0.1, 0.1); 155 | 156 | ApproximationSegmentIterator { count: 1, approximator } 157 | } 158 | 159 | pub fn tangent_arc(_p1: DVec3, _tangent: DVec3, _p3: DVec3) {} 160 | 161 | pub fn edge_type(&self) -> EdgeType { 162 | let curve = ffi::BRepAdaptor_Curve_ctor(&self.inner); 163 | 164 | EdgeType::from(curve.GetType()) 165 | } 166 | } 167 | 168 | pub struct ApproximationSegmentIterator { 169 | count: usize, 170 | approximator: UniquePtr, 171 | } 172 | 173 | impl Iterator for ApproximationSegmentIterator { 174 | type Item = DVec3; 175 | 176 | fn next(&mut self) -> Option { 177 | if self.count <= self.approximator.NbPoints() as usize { 178 | let point = 179 | ffi::GCPnts_TangentialDeflection_Value(&self.approximator, self.count as i32); 180 | 181 | self.count += 1; 182 | Some(dvec3(point.X(), point.Y(), point.Z())) 183 | } else { 184 | None 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives/shell.rs: -------------------------------------------------------------------------------- 1 | use cxx::UniquePtr; 2 | use opencascade_sys::ffi; 3 | 4 | use crate::primitives::Wire; 5 | 6 | pub struct Shell { 7 | pub(crate) inner: UniquePtr, 8 | } 9 | 10 | impl AsRef for Shell { 11 | fn as_ref(&self) -> &Shell { 12 | self 13 | } 14 | } 15 | 16 | impl Shell { 17 | pub(crate) fn from_shell(shell: &ffi::TopoDS_Shell) -> Self { 18 | let inner = ffi::TopoDS_Shell_to_owned(shell); 19 | 20 | Self { inner } 21 | } 22 | 23 | pub fn loft>(wires: impl IntoIterator) -> Self { 24 | let is_solid = false; 25 | let mut make_loft = ffi::BRepOffsetAPI_ThruSections_ctor(is_solid); 26 | 27 | for wire in wires.into_iter() { 28 | make_loft.pin_mut().AddWire(&wire.as_ref().inner); 29 | } 30 | 31 | // Set CheckCompatibility to `true` to avoid twisted results. 32 | make_loft.pin_mut().CheckCompatibility(true); 33 | 34 | let shape = make_loft.pin_mut().Shape(); 35 | let shell = ffi::TopoDS_cast_to_shell(shape); 36 | 37 | Self::from_shell(shell) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives/solid.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | primitives::{BooleanShape, Compound, Edge, Face, Shape, Wire}, 3 | Error, 4 | }; 5 | use cxx::UniquePtr; 6 | use glam::{dvec3, DVec3}; 7 | use opencascade_sys::ffi; 8 | 9 | pub struct Solid { 10 | pub(crate) inner: UniquePtr, 11 | } 12 | 13 | impl AsRef for Solid { 14 | fn as_ref(&self) -> &Solid { 15 | self 16 | } 17 | } 18 | 19 | impl Solid { 20 | pub(crate) fn from_solid(solid: &ffi::TopoDS_Solid) -> Self { 21 | let inner = ffi::TopoDS_Solid_to_owned(solid); 22 | 23 | Self { inner } 24 | } 25 | 26 | // TODO(bschwind) - Do some cool stuff from this link: 27 | // https://neweopencascade.wordpress.com/2018/10/17/lets-talk-about-fillets/ 28 | // Key takeaway: Use the `SectionEdges` function to retrieve edges that were 29 | // the result of combining two shapes. 30 | #[must_use] 31 | pub fn fillet_edge(&self, radius: f64, edge: &Edge) -> Compound { 32 | let inner_shape = ffi::cast_solid_to_shape(&self.inner); 33 | 34 | let mut make_fillet = ffi::BRepFilletAPI_MakeFillet_ctor(inner_shape); 35 | make_fillet.pin_mut().add_edge(radius, &edge.inner); 36 | 37 | let filleted_shape = make_fillet.pin_mut().Shape(); 38 | 39 | let compound = ffi::TopoDS_cast_to_compound(filleted_shape); 40 | 41 | Compound::from_compound(compound) 42 | } 43 | 44 | pub fn loft>(wires: impl IntoIterator) -> Self { 45 | let is_solid = true; 46 | let mut make_loft = ffi::BRepOffsetAPI_ThruSections_ctor(is_solid); 47 | 48 | for wire in wires.into_iter() { 49 | make_loft.pin_mut().AddWire(&wire.as_ref().inner); 50 | } 51 | 52 | // Set to CheckCompatibility to `true` to avoid twisted results. 53 | make_loft.pin_mut().CheckCompatibility(true); 54 | 55 | let shape = make_loft.pin_mut().Shape(); 56 | let solid = ffi::TopoDS_cast_to_solid(shape); 57 | 58 | Self::from_solid(solid) 59 | } 60 | 61 | #[must_use] 62 | pub fn subtract(&self, other: &Solid) -> BooleanShape { 63 | let inner_shape = ffi::cast_solid_to_shape(&self.inner); 64 | let other_inner_shape = ffi::cast_solid_to_shape(&other.inner); 65 | 66 | let mut cut_operation = ffi::BRepAlgoAPI_Cut_ctor(inner_shape, other_inner_shape); 67 | 68 | let edge_list = cut_operation.pin_mut().SectionEdges(); 69 | let vec = ffi::shape_list_to_vector(edge_list); 70 | 71 | let mut new_edges = vec![]; 72 | for shape in vec.iter() { 73 | let edge = ffi::TopoDS_cast_to_edge(shape); 74 | new_edges.push(Edge::from_edge(edge)); 75 | } 76 | 77 | let shape = Shape::from_shape(cut_operation.pin_mut().Shape()); 78 | 79 | BooleanShape { shape, new_edges } 80 | } 81 | 82 | #[must_use] 83 | pub fn union(&self, other: &Solid) -> BooleanShape { 84 | let inner_shape = ffi::cast_solid_to_shape(&self.inner); 85 | let other_inner_shape = ffi::cast_solid_to_shape(&other.inner); 86 | 87 | let mut fuse_operation = ffi::BRepAlgoAPI_Fuse_ctor(inner_shape, other_inner_shape); 88 | let edge_list = fuse_operation.pin_mut().SectionEdges(); 89 | let vec = ffi::shape_list_to_vector(edge_list); 90 | 91 | let mut new_edges = vec![]; 92 | for shape in vec.iter() { 93 | let edge = ffi::TopoDS_cast_to_edge(shape); 94 | new_edges.push(Edge::from_edge(edge)); 95 | } 96 | 97 | let shape = Shape::from_shape(fuse_operation.pin_mut().Shape()); 98 | 99 | BooleanShape { shape, new_edges } 100 | } 101 | 102 | #[must_use] 103 | pub fn intersect(&self, other: &Solid) -> BooleanShape { 104 | let inner_shape = ffi::cast_solid_to_shape(&self.inner); 105 | let other_inner_shape = ffi::cast_solid_to_shape(&other.inner); 106 | 107 | let mut fuse_operation = ffi::BRepAlgoAPI_Common_ctor(inner_shape, other_inner_shape); 108 | let edge_list = fuse_operation.pin_mut().SectionEdges(); 109 | let vec = ffi::shape_list_to_vector(edge_list); 110 | 111 | let mut new_edges = vec![]; 112 | for shape in vec.iter() { 113 | let edge = ffi::TopoDS_cast_to_edge(shape); 114 | new_edges.push(Edge::from_edge(edge)); 115 | } 116 | 117 | let shape = Shape::from_shape(fuse_operation.pin_mut().Shape()); 118 | 119 | BooleanShape { shape, new_edges } 120 | } 121 | 122 | /// Purposefully underpowered for now, this simply takes a list of points, 123 | /// creates a face out of them, and then extrudes it by h in the positive Z 124 | /// direction. 125 | pub fn extrude_polygon( 126 | points: impl IntoIterator, 127 | h: f64, 128 | ) -> Result { 129 | let wire = Wire::from_ordered_points(points)?; 130 | Ok(Face::from_wire(&wire).extrude(dvec3(0.0, 0.0, h))) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives/surface.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::make_point; 2 | use cxx::UniquePtr; 3 | use glam::DVec3; 4 | use opencascade_sys::ffi; 5 | 6 | pub struct Surface { 7 | pub(crate) inner: UniquePtr, 8 | } 9 | 10 | impl Surface { 11 | pub fn bezier(poles: impl IntoIterator>) -> Self { 12 | let poles: Vec> = 13 | poles.into_iter().map(|poles| poles.into_iter().collect()).collect(); 14 | 15 | let mut pole_array = ffi::TColgp_Array2OfPnt_ctor( 16 | 0, 17 | poles.len() as i32 - 1, 18 | 0, 19 | poles.first().map(|first| first.len()).unwrap_or(0) as i32 - 1, 20 | ); 21 | 22 | for (row, poles) in poles.iter().enumerate() { 23 | for (column, pole) in poles.iter().enumerate() { 24 | let pole = &make_point(*pole); 25 | pole_array.pin_mut().SetValue(row as i32, column as i32, pole); 26 | } 27 | } 28 | 29 | let bezier = ffi::Geom_BezierSurface_ctor(&pole_array); 30 | let inner = ffi::bezier_to_surface(&bezier); 31 | 32 | Self { inner } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives/vertex.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::make_point; 2 | use cxx::UniquePtr; 3 | use glam::DVec3; 4 | use opencascade_sys::ffi; 5 | 6 | pub struct Vertex { 7 | pub(crate) inner: UniquePtr, 8 | } 9 | 10 | // You'll see several of these `impl AsRef` blocks for the various primitive 11 | // geometry types. This is for functions which take an Iterator of primitives 12 | // which are either owned or borrowed values. The general pattern looks like this: 13 | // 14 | // pub fn do_something_with_edges>(edges: impl IntoIterator) { 15 | // for edge in edges.into_iter() { 16 | // let edge_ref = edge.as_ref(); 17 | // // Do something with edge_ref 18 | // } 19 | // } 20 | impl AsRef for Vertex { 21 | fn as_ref(&self) -> &Vertex { 22 | self 23 | } 24 | } 25 | 26 | impl Vertex { 27 | pub fn new(point: DVec3) -> Self { 28 | let mut make_vertex = ffi::BRepBuilderAPI_MakeVertex_gp_Pnt(&make_point(point)); 29 | let vertex = make_vertex.pin_mut().Vertex(); 30 | let inner = ffi::TopoDS_Vertex_to_owned(vertex); 31 | 32 | Self { inner } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/opencascade/src/primitives/wire.rs: -------------------------------------------------------------------------------- 1 | use std::iter::once; 2 | 3 | use crate::{ 4 | angle::{Angle, ToAngle}, 5 | law_function::law_function_from_graph, 6 | make_pipe_shell::make_pipe_shell_with_law_function, 7 | primitives::{make_dir, make_point, make_vec, Edge, Face, JoinType, Shape, Shell}, 8 | Error, 9 | }; 10 | use cxx::UniquePtr; 11 | use glam::{dvec3, DVec3}; 12 | use opencascade_sys::ffi; 13 | 14 | pub struct Wire { 15 | pub(crate) inner: UniquePtr, 16 | } 17 | 18 | impl AsRef for Wire { 19 | fn as_ref(&self) -> &Wire { 20 | self 21 | } 22 | } 23 | 24 | /// Provides control over how an edge is considered "connected" to another edge. 25 | #[derive(Debug, Copy, Clone, PartialEq)] 26 | pub enum EdgeConnection { 27 | /// The edges must share the same exact vertices to be considered connected. 28 | Exact, 29 | 30 | /// The endpoints of two edges must be with `tolerance` distance to be considered connected. 31 | Fuzzy { tolerance: f64 }, 32 | } 33 | 34 | impl Default for EdgeConnection { 35 | fn default() -> Self { 36 | Self::Fuzzy { tolerance: 0.001 } 37 | } 38 | } 39 | 40 | impl Wire { 41 | pub(crate) fn from_wire(wire: &ffi::TopoDS_Wire) -> Self { 42 | let inner = ffi::TopoDS_Wire_to_owned(wire); 43 | 44 | Self { inner } 45 | } 46 | 47 | fn from_make_wire(mut make_wire: UniquePtr) -> Self { 48 | Self::from_wire(make_wire.pin_mut().Wire()) 49 | } 50 | 51 | pub fn from_ordered_points(points: impl IntoIterator) -> Result { 52 | let points: Vec<_> = points.into_iter().collect(); 53 | if points.len() < 2 { 54 | return Err(Error::NotEnoughPoints); 55 | } 56 | 57 | let (first, last) = (points.first().unwrap(), points.last().unwrap()); 58 | let mut make_wire = ffi::BRepBuilderAPI_MakeWire_ctor(); 59 | 60 | if points.len() == 2 { 61 | make_wire.pin_mut().add_edge(&Edge::segment(*first, *last).inner); 62 | } else { 63 | for window in points.windows(2).chain(once([*last, *first].as_slice())) { 64 | let edge = Edge::segment(window[0], window[1]); 65 | make_wire.pin_mut().add_edge(&edge.inner); 66 | } 67 | } 68 | 69 | Ok(Self::from_make_wire(make_wire)) 70 | } 71 | 72 | pub fn from_edges<'a>(edges: impl IntoIterator) -> Self { 73 | let mut make_wire = ffi::BRepBuilderAPI_MakeWire_ctor(); 74 | 75 | for edge in edges.into_iter() { 76 | make_wire.pin_mut().add_edge(&edge.inner); 77 | } 78 | 79 | Self::from_make_wire(make_wire) 80 | } 81 | 82 | pub fn from_unordered_edges>( 83 | unordered_edges: impl IntoIterator, 84 | edge_connection: EdgeConnection, 85 | ) -> Self { 86 | let mut edges = ffi::new_HandleTopTools_HSequenceOfShape(); 87 | 88 | for edge in unordered_edges { 89 | let edge_shape = ffi::cast_edge_to_shape(&edge.as_ref().inner); 90 | ffi::TopTools_HSequenceOfShape_append(edges.pin_mut(), edge_shape); 91 | } 92 | 93 | let mut wires = ffi::new_HandleTopTools_HSequenceOfShape(); 94 | 95 | let (tolerance, shared) = match edge_connection { 96 | EdgeConnection::Exact => (0.0, true), 97 | EdgeConnection::Fuzzy { tolerance } => (tolerance, false), 98 | }; 99 | 100 | ffi::connect_edges_to_wires(edges.pin_mut(), tolerance, shared, wires.pin_mut()); 101 | 102 | let mut make_wire = ffi::BRepBuilderAPI_MakeWire_ctor(); 103 | 104 | let wire_len = ffi::TopTools_HSequenceOfShape_length(&wires); 105 | 106 | for index in 1..=wire_len { 107 | let wire_shape = ffi::TopTools_HSequenceOfShape_value(&wires, index); 108 | let wire = ffi::TopoDS_cast_to_wire(wire_shape); 109 | 110 | make_wire.pin_mut().add_wire(wire); 111 | } 112 | 113 | Self::from_make_wire(make_wire) 114 | } 115 | 116 | pub fn from_wires<'a>(wires: impl IntoIterator) -> Self { 117 | let mut make_wire = ffi::BRepBuilderAPI_MakeWire_ctor(); 118 | 119 | for wire in wires.into_iter() { 120 | make_wire.pin_mut().add_wire(&wire.inner); 121 | } 122 | 123 | Self::from_make_wire(make_wire) 124 | } 125 | 126 | #[must_use] 127 | pub fn mirror_along_axis(&self, axis_origin: DVec3, axis_dir: DVec3) -> Self { 128 | let axis_dir = make_dir(axis_dir); 129 | let axis = ffi::gp_Ax1_ctor(&make_point(axis_origin), &axis_dir); 130 | 131 | let mut transform = ffi::new_transform(); 132 | 133 | transform.pin_mut().set_mirror_axis(&axis); 134 | 135 | let wire_shape = ffi::cast_wire_to_shape(&self.inner); 136 | 137 | let mut brep_transform = ffi::BRepBuilderAPI_Transform_ctor(wire_shape, &transform, false); 138 | 139 | let mirrored_shape = brep_transform.pin_mut().Shape(); 140 | let mirrored_wire = ffi::TopoDS_cast_to_wire(mirrored_shape); 141 | 142 | Self::from_wire(mirrored_wire) 143 | } 144 | 145 | pub fn rect(width: f64, height: f64) -> Self { 146 | let half_width = width / 2.0; 147 | let half_height = height / 2.0; 148 | 149 | let p1 = dvec3(-half_width, half_height, 0.0); 150 | let p2 = dvec3(half_width, half_height, 0.0); 151 | let p3 = dvec3(half_width, -half_height, 0.0); 152 | let p4 = dvec3(-half_width, -half_height, 0.0); 153 | 154 | let top = Edge::segment(p1, p2); 155 | let right = Edge::segment(p2, p3); 156 | let bottom = Edge::segment(p3, p4); 157 | let left = Edge::segment(p4, p1); 158 | 159 | Self::from_edges([&top, &right, &bottom, &left]) 160 | } 161 | 162 | #[must_use] 163 | pub fn fillet(&self, radius: f64) -> Wire { 164 | // Create a face from this wire 165 | let face = Face::from_wire(self).fillet(radius); 166 | let inner = ffi::outer_wire(&face.inner); 167 | 168 | Self { inner } 169 | } 170 | 171 | /// Chamfer the wire edges at each vertex by a given distance. 172 | #[must_use] 173 | pub fn chamfer(&self, distance_1: f64) -> Wire { 174 | let face = Face::from_wire(self).chamfer(distance_1); 175 | let inner = ffi::outer_wire(&face.inner); 176 | 177 | Self { inner } 178 | } 179 | 180 | /// Offset the wire by a given distance and join settings 181 | #[must_use] 182 | pub fn offset(&self, distance: f64, join_type: JoinType) -> Self { 183 | let mut make_offset = 184 | ffi::BRepOffsetAPI_MakeOffset_wire_ctor(&self.inner, join_type.into()); 185 | make_offset.pin_mut().Perform(distance, 0.0); 186 | 187 | let offset_shape = make_offset.pin_mut().Shape(); 188 | let result_wire = ffi::TopoDS_cast_to_wire(offset_shape); 189 | 190 | Self::from_wire(result_wire) 191 | } 192 | 193 | /// Sweep the wire along a path to produce a shell 194 | #[must_use] 195 | pub fn sweep_along(&self, path: &Wire) -> Shell { 196 | let profile_shape = ffi::cast_wire_to_shape(&self.inner); 197 | let mut make_pipe = ffi::BRepOffsetAPI_MakePipe_ctor(&path.inner, profile_shape); 198 | 199 | let pipe_shape = make_pipe.pin_mut().Shape(); 200 | let result_shell = ffi::TopoDS_cast_to_shell(pipe_shape); 201 | 202 | Shell::from_shell(result_shell) 203 | } 204 | 205 | /// Sweep the wire along a path, modulated by a function, to produce a shell 206 | #[must_use] 207 | pub fn sweep_along_with_radius_values( 208 | &self, 209 | path: &Wire, 210 | radius_values: impl IntoIterator, 211 | ) -> Shell { 212 | let law_function = law_function_from_graph(radius_values); 213 | let law_handle = ffi::Law_Function_to_handle(law_function); 214 | 215 | let mut make_pipe_shell = 216 | make_pipe_shell_with_law_function(&self.inner, &path.inner, &law_handle); 217 | let pipe_shape = make_pipe_shell.pin_mut().Shape(); 218 | let result_shell = ffi::TopoDS_cast_to_shell(pipe_shape); 219 | 220 | Shell::from_shell(result_shell) 221 | } 222 | 223 | #[must_use] 224 | pub fn translate(&self, offset: DVec3) -> Self { 225 | self.transform(offset, dvec3(1.0, 0.0, 0.0), 0.degrees()) 226 | } 227 | 228 | #[must_use] 229 | pub fn transform(&self, translation: DVec3, rotation_axis: DVec3, angle: Angle) -> Self { 230 | let mut transform = ffi::new_transform(); 231 | let rotation_axis_vec = 232 | ffi::gp_Ax1_ctor(&make_point(DVec3::ZERO), &make_dir(rotation_axis)); 233 | let translation_vec = make_vec(translation); 234 | 235 | transform.pin_mut().SetRotation(&rotation_axis_vec, angle.radians()); 236 | transform.pin_mut().set_translation_vec(&translation_vec); 237 | let location = ffi::TopLoc_Location_from_transform(&transform); 238 | 239 | let wire_shape = ffi::cast_wire_to_shape(&self.inner); 240 | let mut wire_shape = Shape::from_shape(wire_shape).inner; 241 | 242 | let raise_exception = false; 243 | wire_shape.pin_mut().translate(&location, raise_exception); 244 | 245 | let translated_wire = ffi::TopoDS_cast_to_wire(&wire_shape); 246 | 247 | Self::from_wire(translated_wire) 248 | } 249 | 250 | pub fn to_face(self) -> Face { 251 | let only_plane = false; 252 | let make_face = ffi::BRepBuilderAPI_MakeFace_wire(&self.inner, only_plane); 253 | 254 | Face::from_face(make_face.Face()) 255 | } 256 | 257 | // Create a closure-based API 258 | pub fn freeform() {} 259 | } 260 | 261 | pub struct WireBuilder { 262 | inner: UniquePtr, 263 | } 264 | 265 | impl Default for WireBuilder { 266 | fn default() -> Self { 267 | Self::new() 268 | } 269 | } 270 | 271 | impl WireBuilder { 272 | pub fn new() -> Self { 273 | let make_wire = ffi::BRepBuilderAPI_MakeWire_ctor(); 274 | 275 | Self { inner: make_wire } 276 | } 277 | 278 | pub fn add_edge(&mut self, edge: &Edge) { 279 | self.inner.pin_mut().add_edge(&edge.inner); 280 | } 281 | 282 | pub fn build(self) -> Wire { 283 | Wire::from_make_wire(self.inner) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /crates/opencascade/src/section.rs: -------------------------------------------------------------------------------- 1 | use crate::primitives::Shape; 2 | use cxx::UniquePtr; 3 | use opencascade_sys::ffi; 4 | 5 | /// A wrapper around the `BRepAlgoAPI_Section` class. 6 | pub struct Section { 7 | pub(crate) inner: UniquePtr, 8 | } 9 | impl Section { 10 | /// Create a new `Section` to intersect `target` by `tool`. 11 | pub fn new(target: &Shape, tool: &Shape) -> Section { 12 | Section { inner: ffi::BRepAlgoAPI_Section_ctor(&target.inner, &tool.inner) } 13 | } 14 | 15 | /// Get the edges of the resulting intersection. 16 | pub fn section_edges(self) -> Vec { 17 | let mut ba = ffi::cast_section_to_builderalgo(self.inner); 18 | let edges = ffi::shape_list_to_vector(ba.pin_mut().SectionEdges()); 19 | 20 | let mut vec = vec![]; 21 | 22 | for e in edges.iter() { 23 | vec.push(Shape::from_shape(e)); 24 | } 25 | 26 | vec 27 | } 28 | } 29 | 30 | /// Creates a `Section` from two shapes, performs the intersection, and returns the resulting edges. 31 | pub fn edges(target: &Shape, tool: &Shape) -> Vec { 32 | let section = Section::new(target, tool); 33 | section.section_edges() 34 | } 35 | 36 | #[cfg(test)] 37 | mod test { 38 | use super::*; 39 | use crate::{ 40 | primitives::{IntoShape, ShapeType}, 41 | workplane::Workplane, 42 | }; 43 | use glam::dvec3; 44 | 45 | #[test] 46 | fn section_new() { 47 | let a = Workplane::xy().rect(1.0, 1.0).to_face(); 48 | let b = Workplane::yz().rect(1.0, 1.0).to_face(); 49 | 50 | let s = Section::new(&a.into_shape(), &b.into_shape()); 51 | 52 | let edges = s.section_edges(); 53 | assert_eq!(edges.len(), 1); 54 | 55 | let s = edges.first().unwrap(); 56 | 57 | assert_eq!(s.shape_type(), ShapeType::Edge); 58 | 59 | let e = s.edges().next().expect("There should be only one edge"); 60 | 61 | assert_eq!(e.start_point(), dvec3(0.0, -0.5, 0.0)); 62 | assert_eq!(e.end_point(), dvec3(0.0, 0.5, 0.0)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/opencascade/src/workplane.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | angle::{Angle, RVec}, 3 | primitives::{Edge, Wire}, 4 | }; 5 | use glam::{dvec3, DAffine3, DMat3, DVec3, EulerRot}; 6 | 7 | #[derive(Debug, Copy, Clone)] 8 | pub enum Plane { 9 | XY, 10 | YZ, 11 | ZX, 12 | XZ, 13 | YX, 14 | ZY, 15 | Front, 16 | Back, 17 | Left, 18 | Right, 19 | Top, 20 | Bottom, 21 | Custom { x_dir: (f64, f64, f64), normal_dir: (f64, f64, f64) }, 22 | } 23 | 24 | impl Plane { 25 | pub fn transform_point(&self, point: DVec3) -> DVec3 { 26 | self.transform().transform_point3(point) 27 | } 28 | 29 | pub fn transform(&self) -> DAffine3 { 30 | match self { 31 | Self::XY => DAffine3::from_cols(DVec3::X, DVec3::Y, DVec3::Z, DVec3::ZERO), 32 | Self::YZ => DAffine3::from_cols(DVec3::Y, DVec3::Z, DVec3::X, DVec3::ZERO), 33 | Self::ZX => DAffine3::from_cols(DVec3::Z, DVec3::X, DVec3::Y, DVec3::ZERO), 34 | Self::XZ => DAffine3::from_cols(DVec3::X, DVec3::Z, DVec3::NEG_Y, DVec3::ZERO), 35 | Self::YX => DAffine3::from_cols(DVec3::Y, DVec3::X, DVec3::NEG_Z, DVec3::ZERO), 36 | Self::ZY => DAffine3::from_cols(DVec3::Z, DVec3::Y, DVec3::NEG_X, DVec3::ZERO), 37 | Self::Front => DAffine3::from_cols(DVec3::X, DVec3::Y, DVec3::Z, DVec3::ZERO), 38 | Self::Back => DAffine3::from_cols(DVec3::NEG_X, DVec3::Y, DVec3::NEG_Z, DVec3::ZERO), 39 | Self::Left => DAffine3::from_cols(DVec3::Z, DVec3::Y, DVec3::NEG_X, DVec3::ZERO), 40 | Self::Right => DAffine3::from_cols(DVec3::NEG_Z, DVec3::Y, DVec3::X, DVec3::ZERO), 41 | Self::Top => DAffine3::from_cols(DVec3::X, DVec3::NEG_Z, DVec3::Y, DVec3::ZERO), 42 | Self::Bottom => DAffine3::from_cols(DVec3::X, DVec3::Z, DVec3::NEG_Y, DVec3::ZERO), 43 | Self::Custom { x_dir, normal_dir } => { 44 | let x_axis = dvec3(x_dir.0, x_dir.1, x_dir.2).normalize(); 45 | let z_axis = dvec3(normal_dir.0, normal_dir.1, normal_dir.2).normalize(); 46 | let y_axis = z_axis.cross(x_axis).normalize(); 47 | 48 | DAffine3::from_cols(x_axis, y_axis, z_axis, DVec3::ZERO) 49 | }, 50 | } 51 | } 52 | } 53 | 54 | #[derive(Debug, Clone)] 55 | pub struct Workplane { 56 | transform: DAffine3, 57 | } 58 | 59 | impl Workplane { 60 | pub fn new(x_dir: DVec3, normal_dir: DVec3) -> Self { 61 | Self { 62 | transform: Plane::Custom { 63 | x_dir: x_dir.normalize().into(), 64 | normal_dir: normal_dir.normalize().into(), 65 | } 66 | .transform(), 67 | } 68 | } 69 | 70 | pub fn xy() -> Self { 71 | Self { transform: Plane::XY.transform() } 72 | } 73 | 74 | pub fn yz() -> Self { 75 | Self { transform: Plane::YZ.transform() } 76 | } 77 | 78 | pub fn zx() -> Self { 79 | Self { transform: Plane::ZX.transform() } 80 | } 81 | 82 | pub fn xz() -> Self { 83 | Self { transform: Plane::XZ.transform() } 84 | } 85 | 86 | pub fn zy() -> Self { 87 | Self { transform: Plane::ZY.transform() } 88 | } 89 | 90 | pub fn yx() -> Self { 91 | Self { transform: Plane::YX.transform() } 92 | } 93 | 94 | pub fn origin(&self) -> DVec3 { 95 | self.transform.translation 96 | } 97 | 98 | pub fn normal(&self) -> DVec3 { 99 | self.transform.matrix3.z_axis 100 | } 101 | 102 | pub fn x_dir(&self) -> DVec3 { 103 | self.transform.matrix3.x_axis 104 | } 105 | 106 | pub fn y_dir(&self) -> DVec3 { 107 | self.transform.matrix3.y_axis 108 | } 109 | 110 | // TODO(bschwind) - Test this. 111 | pub fn set_rotation(&mut self, (rot_x, rot_y, rot_z): (Angle, Angle, Angle)) { 112 | let rotation_matrix = 113 | DMat3::from_euler(EulerRot::XYZ, rot_x.radians(), rot_y.radians(), rot_z.radians()); 114 | 115 | let translation = self.transform.translation; 116 | 117 | let x_dir = DVec3::X; 118 | let normal_dir = DVec3::Z; 119 | 120 | self.transform = Plane::Custom { 121 | x_dir: rotation_matrix.mul_vec3(x_dir).into(), 122 | normal_dir: rotation_matrix.mul_vec3(normal_dir).into(), 123 | } 124 | .transform(); 125 | 126 | self.set_translation(translation); 127 | } 128 | 129 | pub fn rotate_by(&mut self, (rot_x, rot_y, rot_z): (Angle, Angle, Angle)) { 130 | let rotation_matrix = 131 | DMat3::from_euler(EulerRot::XYZ, rot_x.radians(), rot_y.radians(), rot_z.radians()); 132 | 133 | let translation = self.transform.translation; 134 | 135 | let x_dir = rotation_matrix.mul_vec3(DVec3::X); 136 | let normal_dir = rotation_matrix.mul_vec3(DVec3::Z); 137 | 138 | self.transform = Plane::Custom { 139 | x_dir: self.transform.transform_vector3(x_dir).into(), 140 | normal_dir: self.transform.transform_vector3(normal_dir).into(), 141 | } 142 | .transform(); 143 | 144 | self.set_translation(translation); 145 | } 146 | 147 | pub fn set_translation(&mut self, pos: DVec3) { 148 | self.transform.translation = pos; 149 | } 150 | 151 | pub fn translate_by(&mut self, offset: DVec3) { 152 | self.transform.translation += offset; 153 | } 154 | 155 | pub fn transformed(&self, offset: DVec3, rotate: RVec) -> Self { 156 | let mut new = self.clone(); 157 | let new_origin = new.to_world_pos(offset); 158 | 159 | new.rotate_by((rotate.x, rotate.y, rotate.z)); 160 | new.transform.translation = new_origin; 161 | 162 | new 163 | } 164 | 165 | pub fn translated(&self, offset: DVec3) -> Self { 166 | let mut new = self.clone(); 167 | let new_origin = new.to_world_pos(offset); 168 | new.transform.translation = new_origin; 169 | 170 | new 171 | } 172 | 173 | pub fn rotated(&self, rotate: RVec) -> Self { 174 | let mut new = self.clone(); 175 | new.rotate_by((rotate.x, rotate.y, rotate.z)); 176 | 177 | new 178 | } 179 | 180 | pub fn to_world_pos(&self, pos: DVec3) -> DVec3 { 181 | self.transform.transform_point3(pos) 182 | } 183 | 184 | pub fn to_local_pos(&self, pos: DVec3) -> DVec3 { 185 | self.transform.inverse().transform_point3(pos) 186 | } 187 | 188 | pub fn rect(&self, width: f64, height: f64) -> Wire { 189 | let half_width = width / 2.0; 190 | let half_height = height / 2.0; 191 | 192 | let p1 = self.to_world_pos(dvec3(-half_width, half_height, 0.0)); 193 | let p2 = self.to_world_pos(dvec3(half_width, half_height, 0.0)); 194 | let p3 = self.to_world_pos(dvec3(half_width, -half_height, 0.0)); 195 | let p4 = self.to_world_pos(dvec3(-half_width, -half_height, 0.0)); 196 | 197 | let top = Edge::segment(p1, p2); 198 | let right = Edge::segment(p2, p3); 199 | let bottom = Edge::segment(p3, p4); 200 | let left = Edge::segment(p4, p1); 201 | 202 | Wire::from_edges([&top, &right, &bottom, &left]) 203 | } 204 | 205 | pub fn circle(&self, x: f64, y: f64, radius: f64) -> Wire { 206 | let center = self.to_world_pos(dvec3(x, y, 0.0)); 207 | 208 | let circle = Edge::circle(center, self.normal(), radius); 209 | 210 | Wire::from_edges([&circle]) 211 | } 212 | 213 | pub fn sketch(&self) -> Sketch { 214 | let cursor = self.to_world_pos(DVec3::ZERO); 215 | Sketch::new(cursor, self.clone()) 216 | } 217 | } 218 | 219 | pub struct Sketch { 220 | first_point: Option, 221 | cursor: DVec3, // cursor is in global coordinates 222 | workplane: Workplane, 223 | edges: Vec, 224 | } 225 | 226 | impl Sketch { 227 | fn new(cursor: DVec3, workplane: Workplane) -> Self { 228 | Self { first_point: None, cursor, workplane, edges: Vec::new() } 229 | } 230 | 231 | fn add_edge(&mut self, edge: Edge) { 232 | if self.first_point.is_none() { 233 | self.first_point = Some(edge.start_point()); 234 | } 235 | 236 | self.edges.push(edge); 237 | } 238 | 239 | pub fn move_to(mut self, x: f64, y: f64) -> Self { 240 | self.cursor = self.workplane.to_world_pos(dvec3(x, y, 0.0)); 241 | self 242 | } 243 | 244 | pub fn line_to(mut self, x: f64, y: f64) -> Self { 245 | let new_point = self.workplane.to_world_pos(dvec3(x, y, 0.0)); 246 | let new_edge = Edge::segment(self.cursor, new_point); 247 | self.cursor = new_point; 248 | 249 | self.add_edge(new_edge); 250 | 251 | self 252 | } 253 | 254 | pub fn line_dx(mut self, dx: f64) -> Self { 255 | let cursor = self.workplane.to_local_pos(self.cursor); 256 | let new_point = self.workplane.to_world_pos(dvec3(cursor.x + dx, cursor.y, 0.0)); 257 | let new_edge = Edge::segment(self.cursor, new_point); 258 | self.cursor = new_point; 259 | 260 | self.add_edge(new_edge); 261 | 262 | self 263 | } 264 | 265 | pub fn line_dy(mut self, dy: f64) -> Self { 266 | let cursor = self.workplane.to_local_pos(self.cursor); 267 | let new_point = self.workplane.to_world_pos(dvec3(cursor.x, cursor.y + dy, 0.0)); 268 | let new_edge = Edge::segment(self.cursor, new_point); 269 | self.cursor = new_point; 270 | 271 | self.add_edge(new_edge); 272 | 273 | self 274 | } 275 | 276 | pub fn line_dx_dy(mut self, dx: f64, dy: f64) -> Self { 277 | let cursor = self.workplane.to_local_pos(self.cursor); 278 | let new_point = self.workplane.to_world_pos(dvec3(cursor.x + dx, cursor.y + dy, 0.0)); 279 | let new_edge = Edge::segment(self.cursor, new_point); 280 | self.cursor = new_point; 281 | 282 | self.add_edge(new_edge); 283 | 284 | self 285 | } 286 | 287 | pub fn arc(mut self, (x1, y1): (f64, f64), (x2, y2): (f64, f64), (x3, y3): (f64, f64)) -> Self { 288 | let p1 = self.workplane.to_world_pos(dvec3(x1, y1, 0.0)); 289 | let p2 = self.workplane.to_world_pos(dvec3(x2, y2, 0.0)); 290 | let p3 = self.workplane.to_world_pos(dvec3(x3, y3, 0.0)); 291 | 292 | let new_arc = Edge::arc(p1, p2, p3); 293 | 294 | self.cursor = p3; 295 | 296 | self.add_edge(new_arc); 297 | 298 | self 299 | } 300 | 301 | pub fn three_point_arc(self, p2: (f64, f64), p3: (f64, f64)) -> Self { 302 | let cursor = self.workplane.to_local_pos(self.cursor); 303 | self.arc((cursor.x, cursor.y), p2, p3) 304 | } 305 | 306 | pub fn wire(self) -> Wire { 307 | Wire::from_edges(&self.edges) 308 | } 309 | 310 | pub fn close(mut self) -> Wire { 311 | let start_point = self.first_point.unwrap(); 312 | 313 | let new_edge = Edge::segment(self.cursor, start_point); 314 | self.add_edge(new_edge); 315 | Wire::from_edges(&self.edges) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /crates/viewer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "viewer" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1" 8 | bytemuck = { version = "1", features = ["derive"] } 9 | clap = { version = "4", features = ["derive"] } 10 | examples = { path = "../../examples", default-features = false } 11 | glam = { version = "0.24", features = ["bytemuck"] } 12 | kicad-parser = { path = "../kicad-parser" } 13 | notify = "6" 14 | opencascade = { version = "0.2", path = "../opencascade", default-features = false } 15 | simple-game = { git = "https://github.com/bschwind/simple-game.git", rev = "19f800cf5c29a41e44caaab2baf62b5cbddb5ce2" } 16 | smaa = "0.16" 17 | wasmtime = "32" 18 | wgpu = "24" 19 | winit = { version = "0.30", features = ["rwh_05"] } 20 | wit-component = { version = "0.229", default-features = false } 21 | 22 | [build-dependencies] 23 | naga = { version = "24", features = ["wgsl-in"] } 24 | 25 | [features] 26 | default = ["builtin"] 27 | builtin = ["opencascade/builtin", "examples/builtin"] 28 | -------------------------------------------------------------------------------- /crates/viewer/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | const SRC_DIR: &str = "shaders"; 4 | 5 | fn main() { 6 | println!("cargo:rerun-if-changed={}", SRC_DIR); 7 | 8 | for entry in std::fs::read_dir(SRC_DIR).expect("Shaders directory should exist") { 9 | let entry = entry.unwrap(); 10 | let path = entry.path(); 11 | 12 | if let Some(extension) = path.extension().and_then(|os_str| os_str.to_str()) { 13 | if extension.to_ascii_lowercase().as_str() == "wgsl" { 14 | println!("cargo:rerun-if-changed={}", path.to_string_lossy()); 15 | compile_shader(path); 16 | } 17 | } 18 | } 19 | } 20 | 21 | fn compile_shader>(path: P) { 22 | let path = path.as_ref(); 23 | let shader_source = std::fs::read_to_string(path).expect("Shader source should be available"); 24 | 25 | let module = naga::front::wgsl::parse_str(&shader_source) 26 | .inspect_err(|e| { 27 | let msg = e.emit_to_string(&shader_source); 28 | println!("{msg}"); 29 | }) 30 | .expect("Shader compilation failed"); 31 | 32 | let _info = naga::valid::Validator::new( 33 | naga::valid::ValidationFlags::all(), 34 | naga::valid::Capabilities::empty(), 35 | ) 36 | .validate(&module) 37 | .expect("Shader validation failed"); 38 | } 39 | -------------------------------------------------------------------------------- /crates/viewer/shaders/edge.wgsl: -------------------------------------------------------------------------------- 1 | struct Globals { 2 | proj: mat4x4, 3 | transform: mat4x4, 4 | resolution: vec4, // X = screen width, Y = screen height, Z = dash_size, W = gap_size 5 | }; 6 | 7 | // Uniforms 8 | @group(0) @binding(0) 9 | var globals: Globals; 10 | 11 | struct VertexInput { 12 | // Per-vertex data 13 | @location(0) 14 | pos: vec3, 15 | 16 | // Per-instance data 17 | @location(1) 18 | point_a: vec4, 19 | 20 | @location(2) 21 | point_b: vec4, 22 | 23 | @location(3) 24 | lengths_so_far: vec4, 25 | }; 26 | 27 | struct VertexOutput { 28 | @builtin(position) 29 | pos: vec4, 30 | 31 | @location(0) 32 | dist: f32, 33 | }; 34 | 35 | @vertex 36 | fn main_vs(input: VertexInput) -> VertexOutput { 37 | var out: VertexOutput; 38 | 39 | let a_width = input.point_a.w; 40 | let b_width = input.point_b.w; 41 | 42 | // Transform the segment endpoints to clip space 43 | let clip0 = globals.proj * globals.transform * vec4(input.point_a.xyz, 1.0); 44 | let clip1 = globals.proj * globals.transform * vec4(input.point_b.xyz, 1.0); 45 | 46 | // Transform the segment endpoints to screen space 47 | let a = globals.resolution.xy * (0.5 * clip0.xy / clip0.w + 0.5); 48 | let b = globals.resolution.xy * (0.5 * clip1.xy / clip1.w + 0.5); 49 | 50 | let x_basis = normalize(b - a); 51 | let y_basis = vec2(-x_basis.y, x_basis.x); 52 | 53 | let offset_a = a + a_width * (input.pos.x * x_basis + input.pos.y * y_basis); 54 | let offset_b = b + b_width * (input.pos.x * x_basis + input.pos.y * y_basis); 55 | 56 | let final_pos = mix(offset_a, offset_b, vec2(input.pos.z)); 57 | 58 | let clip = mix(clip0, clip1, vec4(input.pos.z)); 59 | 60 | out.pos = vec4(clip.w * ((2.0 * final_pos) / globals.resolution.xy - 1.0), clip.z, clip.w); 61 | out.dist = mix(input.lengths_so_far.x, input.lengths_so_far.y, input.pos.z); 62 | 63 | return out; 64 | } 65 | 66 | @fragment 67 | fn main_fs(input: VertexOutput) -> @location(0) vec4 { 68 | let r = 0.0; 69 | let g = 0.0; 70 | let b = 0.0; 71 | 72 | let dash_size = globals.resolution.z; 73 | let gap_size = globals.resolution.w; 74 | 75 | if (fract(input.dist / (dash_size + gap_size)) > dash_size / (dash_size + gap_size)) { 76 | discard; 77 | } 78 | 79 | return vec4(r, g, b, 1.0); 80 | } 81 | -------------------------------------------------------------------------------- /crates/viewer/shaders/surface.wgsl: -------------------------------------------------------------------------------- 1 | struct Globals { 2 | proj: mat4x4, 3 | transform: mat4x4, 4 | }; 5 | 6 | // Uniforms 7 | @group(0) @binding(0) 8 | var globals: Globals; 9 | 10 | struct VertexInput { 11 | @location(0) 12 | pos: vec3, 13 | 14 | @location(1) 15 | uv: vec2, 16 | 17 | @location(2) 18 | normal: vec3, 19 | }; 20 | 21 | struct VertexOutput { 22 | @builtin(position) 23 | pos: vec4, 24 | 25 | @location(0) 26 | uv: vec2, 27 | 28 | @location(1) 29 | normal: vec3, 30 | }; 31 | 32 | @vertex 33 | fn vs_main(input: VertexInput) -> VertexOutput { 34 | var out: VertexOutput; 35 | 36 | out.uv = input.uv; 37 | out.normal = input.normal; // TODO(bschwind) - Need to transform this. 38 | out.pos = globals.proj * globals.transform * vec4(input.pos, 1.0); 39 | 40 | return out; 41 | } 42 | 43 | @fragment 44 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 45 | return vec4(1.0, 1.0, 1.0, 1.0); 46 | } 47 | -------------------------------------------------------------------------------- /crates/viewer/src/camera.rs: -------------------------------------------------------------------------------- 1 | use glam::{Mat3, Mat4, Quat, Vec2, Vec3}; 2 | 3 | const MIN_ZOOM_FACTOR: f32 = 0.05; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 | enum Projection { 7 | /// Right-handed orthographic projection. 8 | Orthographic, 9 | /// Right-handed Perspective projection. 10 | Perspective, 11 | } 12 | 13 | pub struct OrbitCamera { 14 | projection: Projection, 15 | aspect_ratio: f32, 16 | // Zoom factor used for orthographic projection. 17 | zoom_factor: f32, 18 | // The look-at target, in the center of the view. 19 | target: Vec3, 20 | // The radius of the orbit 21 | radius: f32, 22 | // The orientation of the camera around the target point 23 | orientation: Quat, 24 | } 25 | 26 | impl OrbitCamera { 27 | pub fn new(width: u32, height: u32, init_pos: Vec3) -> Self { 28 | let target = Vec3::ZERO; 29 | let radius = init_pos.length(); 30 | let look_at_matrix = Mat4::look_at_rh(init_pos, target, Vec3::Z); 31 | let orientation = Quat::from_mat4(&look_at_matrix).inverse(); 32 | Self { 33 | projection: Projection::Orthographic, 34 | aspect_ratio: width as f32 / height as f32, 35 | zoom_factor: 1.0, 36 | target, 37 | radius, 38 | orientation, 39 | } 40 | } 41 | 42 | pub fn resize(&mut self, width: u32, height: u32) { 43 | self.aspect_ratio = width as f32 / height as f32; 44 | } 45 | 46 | pub fn use_perspective(&mut self) { 47 | self.projection = Projection::Perspective; 48 | } 49 | 50 | pub fn use_orthographic(&mut self) { 51 | self.projection = Projection::Orthographic; 52 | } 53 | 54 | fn get_local_frame(&self) -> Mat3 { 55 | Mat3::from_quat(self.orientation) 56 | } 57 | 58 | /// Pan the camera view horizontally and vertically. Look-at target will move along with the 59 | /// camera. 60 | pub fn pan(&mut self, delta: Vec2) { 61 | self.target -= self.get_local_frame() * delta.extend(0.0); 62 | } 63 | 64 | /// Zoom in or out, while looking at the same target. 65 | pub fn zoom(&mut self, zoom_delta: f32) { 66 | self.zoom_factor = f32::max(self.zoom_factor * f32::exp(zoom_delta), MIN_ZOOM_FACTOR); 67 | } 68 | 69 | /// Orbit around the target while keeping the distance. 70 | pub fn rotate(&mut self, rotator: Quat) { 71 | self.orientation = (self.orientation * rotator).normalize(); 72 | } 73 | 74 | pub fn matrix(&self) -> Mat4 { 75 | // These magic numbers are configured so that the particular model we are loading is 76 | // visible in its entirety. They will be dynamically computed eventually when we have "fit 77 | // to view" function or alike. 78 | 79 | let (proj, effective_radius) = match self.projection { 80 | Projection::Orthographic => { 81 | let proj = Mat4::orthographic_rh( 82 | -50.0 * self.zoom_factor * self.aspect_ratio, 83 | 50.0 * self.zoom_factor * self.aspect_ratio, 84 | -50.0 * self.zoom_factor, 85 | 50.0 * self.zoom_factor, 86 | -1000.0, 87 | 1000.0, 88 | ); 89 | (proj, self.radius) 90 | }, 91 | Projection::Perspective => { 92 | let proj = Mat4::perspective_rh( 93 | std::f32::consts::PI / 2.0, 94 | self.aspect_ratio, 95 | 1.0, 96 | 10_000.0, 97 | ); 98 | (proj, self.zoom_factor * self.radius) 99 | }, 100 | }; 101 | 102 | let local_frame = self.get_local_frame(); 103 | let position = self.target + effective_radius * local_frame.z_axis; 104 | 105 | // NOTE(mkovaxx): This is computing inverse(translation * orientation), but more efficiently 106 | let view = 107 | Mat4::from_quat(self.orientation.conjugate()) * Mat4::from_translation(-position); 108 | 109 | proj * view 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/viewer/src/surface_drawer.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{Pod, Zeroable}; 2 | use glam::Mat4; 3 | use opencascade::mesh::Mesh; 4 | use simple_game::graphics::GraphicsDevice; 5 | use wgpu::{self, util::DeviceExt, Buffer, RenderPipeline}; 6 | 7 | pub struct SurfaceDrawer { 8 | vertex_uniform: wgpu::Buffer, 9 | uniform_bind_group: wgpu::BindGroup, 10 | pipeline: RenderPipeline, 11 | } 12 | 13 | impl SurfaceDrawer { 14 | pub fn new( 15 | device: &wgpu::Device, 16 | target_format: wgpu::TextureFormat, 17 | depth_format: wgpu::TextureFormat, 18 | ) -> Self { 19 | // Uniform buffer 20 | let cad_mesh_uniforms = CadMeshUniforms::default(); 21 | 22 | let vertex_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 23 | label: Some("Line drawer vertex shader uniform buffer"), 24 | contents: bytemuck::bytes_of(&cad_mesh_uniforms), 25 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 26 | }); 27 | 28 | let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 29 | label: Some("CadMesh bind group layout"), 30 | entries: &[wgpu::BindGroupLayoutEntry { 31 | binding: 0, 32 | visibility: wgpu::ShaderStages::VERTEX, 33 | ty: wgpu::BindingType::Buffer { 34 | ty: wgpu::BufferBindingType::Uniform, 35 | has_dynamic_offset: false, 36 | min_binding_size: wgpu::BufferSize::new( 37 | std::mem::size_of::() as u64 38 | ), 39 | }, 40 | count: None, 41 | }], 42 | }); 43 | 44 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 45 | label: Some("CadMesh pipeline layout"), 46 | bind_group_layouts: &[&bind_group_layout], 47 | push_constant_ranges: &[], 48 | }); 49 | 50 | let vertex_buffers = &[wgpu::VertexBufferLayout { 51 | array_stride: (std::mem::size_of::()) as wgpu::BufferAddress, 52 | step_mode: wgpu::VertexStepMode::Vertex, 53 | attributes: &wgpu::vertex_attr_array![ 54 | 0 => Float32x3, // pos 55 | 1 => Float32x2, // uv 56 | 2 => Float32x3, // normal 57 | ], 58 | }]; 59 | 60 | let draw_shader = 61 | GraphicsDevice::load_wgsl_shader(device, include_str!("../shaders/surface.wgsl")); 62 | 63 | let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 64 | label: Some("CadMesh render pipeline"), 65 | layout: Some(&pipeline_layout), 66 | vertex: wgpu::VertexState { 67 | module: &draw_shader, 68 | entry_point: Some("vs_main"), 69 | buffers: vertex_buffers, 70 | compilation_options: wgpu::PipelineCompilationOptions::default(), 71 | }, 72 | primitive: wgpu::PrimitiveState { 73 | topology: wgpu::PrimitiveTopology::TriangleList, 74 | // strip_index_format: Some(wgpu::IndexFormat::Uint32), 75 | front_face: wgpu::FrontFace::Ccw, 76 | cull_mode: Some(wgpu::Face::Back), 77 | polygon_mode: wgpu::PolygonMode::Fill, 78 | conservative: false, 79 | ..wgpu::PrimitiveState::default() 80 | }, 81 | depth_stencil: Some(wgpu::DepthStencilState { 82 | format: depth_format, 83 | depth_write_enabled: true, 84 | depth_compare: wgpu::CompareFunction::Less, 85 | stencil: wgpu::StencilState::default(), 86 | bias: wgpu::DepthBiasState::default(), 87 | }), 88 | multisample: wgpu::MultisampleState { 89 | count: 1, 90 | mask: !0, 91 | alpha_to_coverage_enabled: false, 92 | }, 93 | fragment: Some(wgpu::FragmentState { 94 | module: &draw_shader, 95 | entry_point: Some("fs_main"), 96 | targets: &[Some(wgpu::ColorTargetState { 97 | format: target_format, 98 | blend: Some(wgpu::BlendState { 99 | color: wgpu::BlendComponent::REPLACE, 100 | alpha: wgpu::BlendComponent::REPLACE, 101 | }), 102 | write_mask: wgpu::ColorWrites::ALL, 103 | })], 104 | compilation_options: wgpu::PipelineCompilationOptions::default(), 105 | }), 106 | multiview: None, 107 | cache: None, 108 | }); 109 | 110 | let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 111 | layout: &pipeline.get_bind_group_layout(0), 112 | entries: &[wgpu::BindGroupEntry { 113 | binding: 0, 114 | resource: vertex_uniform.as_entire_binding(), 115 | }], 116 | label: None, 117 | }); 118 | 119 | Self { vertex_uniform, uniform_bind_group, pipeline } 120 | } 121 | 122 | #[allow(clippy::too_many_arguments)] 123 | pub fn render( 124 | &self, 125 | render_pass: &mut wgpu::RenderPass, 126 | queue: &wgpu::Queue, 127 | cad_mesh: &CadMesh, 128 | camera_matrix: Mat4, 129 | transform: Mat4, 130 | ) { 131 | let uniforms = CadMeshUniforms { proj: camera_matrix, transform }; 132 | 133 | queue.write_buffer(&self.vertex_uniform, 0, bytemuck::bytes_of(&uniforms)); 134 | 135 | render_pass.set_pipeline(&self.pipeline); 136 | render_pass.set_bind_group(0, &self.uniform_bind_group, &[]); 137 | 138 | // TODO(bschwind) - This is to fix this issue with wgpu: 139 | // https://github.com/gfx-rs/wgpu/issues/6779 140 | if cad_mesh.vertex_buf.size() == 0 || cad_mesh.index_buf.size() == 0 { 141 | return; 142 | } 143 | render_pass.set_index_buffer(cad_mesh.index_buf.slice(..), wgpu::IndexFormat::Uint32); 144 | render_pass.set_vertex_buffer(0, cad_mesh.vertex_buf.slice(..)); 145 | render_pass.draw_indexed(0..(cad_mesh.num_indices as u32), 0, 0..1); 146 | } 147 | } 148 | 149 | #[repr(C)] 150 | #[derive(Default, Debug, Copy, Clone, Pod, Zeroable)] 151 | struct CadMeshUniforms { 152 | proj: Mat4, 153 | transform: Mat4, 154 | } 155 | 156 | #[repr(C)] 157 | #[derive(Clone, Copy, Pod, Zeroable)] 158 | struct CadMeshVertex { 159 | pos: [f32; 3], 160 | uv: [f32; 2], 161 | normal: [f32; 3], 162 | } 163 | 164 | pub struct CadMesh { 165 | num_indices: usize, 166 | vertex_buf: Buffer, 167 | index_buf: Buffer, 168 | } 169 | 170 | impl CadMesh { 171 | pub fn from_mesh(mesh: &Mesh, device: &wgpu::Device) -> Self { 172 | let vertex_data: Vec<_> = mesh 173 | .vertices 174 | .iter() 175 | .zip(mesh.uvs.iter()) 176 | .zip(mesh.normals.iter()) 177 | .map(|((v, uv), normal)| CadMeshVertex { 178 | pos: [v.x as f32, v.y as f32, v.z as f32], 179 | uv: [uv.x as f32, uv.y as f32], 180 | normal: [normal.x as f32, normal.y as f32, normal.z as f32], 181 | }) 182 | .collect(); 183 | 184 | let index_data: Vec<_> = mesh.indices.iter().map(|i| *i as u32).collect(); 185 | 186 | let vertex_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 187 | label: Some("CadMesh Vertex Buffer"), 188 | contents: bytemuck::cast_slice(&vertex_data), 189 | usage: wgpu::BufferUsages::VERTEX, 190 | }); 191 | 192 | let index_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 193 | label: Some("CadMesh Index Buffer"), 194 | contents: bytemuck::cast_slice(&index_data), 195 | usage: wgpu::BufferUsages::INDEX, 196 | }); 197 | 198 | Self { num_indices: mesh.indices.len(), vertex_buf, index_buf } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /crates/wasm-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | glam = { version = "0.24" } 8 | model-api = { path = "../model-api" } 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | -------------------------------------------------------------------------------- /crates/wasm-example/README.md: -------------------------------------------------------------------------------- 1 | # wasm-example 2 | 3 | An example of model code which can be compiled to WASM and executed in the viewer app. 4 | 5 | Use [cargo watch](https://crates.io/crates/cargo-watch) to listen for file changes and rebuild the model file whenever code is edited: 6 | 7 | ``` 8 | # From the project root 9 | $ cargo watch --delay 0 -x "build -p wasm-example --release --target wasm32-unknown-unknown" 10 | ``` 11 | 12 | Run the viewer app with: 13 | 14 | ``` 15 | # From the project root, while the "cargo watch" command above is running. 16 | $ cargo run --release --bin viewer -- --wasm-path target/wasm32-unknown-unknown/release/wasm_example.wasm 17 | ``` 18 | 19 | Edit `src/lib.rs` and save to see the model code get recompiled, and see the model in the viewer app get updated. 20 | -------------------------------------------------------------------------------- /crates/wasm-example/src/lib.rs: -------------------------------------------------------------------------------- 1 | use glam::dvec3; 2 | use model_api::{ 3 | primitives::{Direction, IntoShape, Shape}, 4 | workplane::Workplane, 5 | Model, 6 | }; 7 | 8 | struct RoundedChamfer {} 9 | 10 | impl Model for RoundedChamfer { 11 | fn new() -> Self { 12 | Self {} 13 | } 14 | 15 | fn create_model(&mut self) -> Shape { 16 | let shape = Workplane::xy() 17 | .rect(16.0, 10.0) 18 | .fillet(1.0) 19 | .to_face() 20 | .extrude(dvec3(0.0, 0.0, 3.0)) 21 | .into_shape(); 22 | 23 | let top_edges = shape.faces().farthest(Direction::PosZ).edges(); 24 | 25 | shape.chamfer_edges(0.7, top_edges) 26 | } 27 | } 28 | 29 | model_api::register_model!(RoundedChamfer); 30 | -------------------------------------------------------------------------------- /docs/images/cascade_library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bschwind/opencascade-rs/d1db1bf1fb58dd094144532aa0e5c22106d61083/docs/images/cascade_library.png -------------------------------------------------------------------------------- /docs/writing_bindings.md: -------------------------------------------------------------------------------- 1 | # Binding to OpenCascade Classes and Functions 2 | 3 | OpenCascade is a huge C++ project, spanning multiple decades. It has an impressive amount of functionality packed in, though it is somewhat hidden behind a (in my opinion) ugly/intimidating API. 4 | 5 | One goal of this project is to expose a more ergonomic and approachable API on top of the goodies within OpenCascade so more people can use it to build cool things. OpenCascade leans pretty hard into C++, and so we need to use something more than just `bindgen` in order to have Rust call it safely. 6 | 7 | For that we use [CXX](https://cxx.rs/), by dtolnay. CXX itself is also somewhat intimidating and hard to figure out on first use, so this document will hopefully show how _this_ project uses it to bind to OpenCascade and expose useful functionality. 8 | 9 | ## Organization 10 | 11 | ### `build.rs` 12 | 13 | At the very bottom, we have the [occt-sys crate](../crates/occt-sys/), which builds OpenCascade C++ project statically without any modifications. 14 | 15 | Right above this is [opencascade-sys crate](../crates/opencascade-sys), which has a [build.rs](../crates/opencascade-sys/build.rs) file with necessary `cxx` infrastructure to compile its wrapper and C++ bindings to OpenCascade. 16 | 17 | In the theme of keeping it minimal, the OpenCascade libraries we statically link to are all explicitly laid out with `cargo:rustc-link-lib=static=LIB_NAME_HERE` cargo directives. 18 | 19 | ### C++/Rust Bridge File 20 | 21 | In order to expose C++ types to Rust, and vice-versa, we need to write what cxx.rs calls a "bridge" file or module. This project currently has [one giant bridge file](../crates/opencascade-sys/src/lib.rs), but in the interest of organization this will eventually be broken up into sensible modules. 22 | 23 | At the very top of the bridge module we can define types that are visible to both C++ and Rust, such as simple enums which can be represented as `u32`, for example. 24 | 25 | Pretty much everything else goes inside of an `unsafe extern "C++" {}` block. Inside of this block, we declare opaque C++ types that we want Rust to know about. It is enough to simply state `type SOME_CPP_TYPE_HERE;` to make this declaration. `cxx` will then search included headers and make sure that type exists in the C++ world. With just the type, you can't really do anything, so you also need to start declaring functions that _should_ exist in the C++ world which you want to use. 26 | 27 | There are some rules to how we define these functions in our code: 28 | 29 | * At no point can a function in the bridge return a bare, owned C++ type. It must be behind an immutable reference, or a smart pointer such as `UniquePtr` or `SharedPtr`. 30 | * If you're binding to a class method (not a free-floating function), you must use the `self` keyword as the first argument to the function. 31 | * If the method is `const` on the C++ side (not modifying `self`), then `fn do_something(self: &TheCPPTypeHere)` is sufficient. 32 | * If the method is _not_ `const`, then the signature must be `fn do_something(self: Pin<&mut TheCPPTypeHere>)` 33 | * Getting this wrong will result in ugly C++ compile errors getting spewed out on the console. 34 | * If you're binding to a free-floating function, then avoid using the special `self` keyword as the name for a function argument, and just use `&T` and `Pin<&mut T>` as appropriate. 35 | * From what I can tell, generics don't work from Rust to C++ templates. If you have a C++ type called `Handle`, you'll need to declare your own C++ type called `HandleEdge` or whatever you want, and alias it to the full type (ex:`typedef opencascade::handle HandleEdge;` 36 | * You can use `#[cxx_name = "SomeCPPFunctionName"]` to tell `cxx` the _real_ name of the C++ function you want to use, and `#[rust_name = "some_rust_fn_name"]` to control what the name of the function is exposed to the Rust side of things. If you don't use these attributes, the exported Rust function is exactly the same as the C++ function name. 37 | 38 | #### Getting a `Pin<&mut T>` 39 | 40 | If you have a `some_var: UniquePtr`, you can get a `Pin<&mut T>` by calling `some_var.pin_mut()`. `some_var` must be declared as `mut` to use this. 41 | 42 | #### `construct_unique` 43 | 44 | Providing bindings to C++ constructors is somewhat [tricky](https://github.com/dtolnay/cxx/issues/280) and not well supported in cxx. At the same time, we can't have functions with return `T` directly, it needs to return either a reference or a smart pointer as stated previously. 45 | 46 | So in practice, most constructor functions exposed in this crate return a `UniquePtr`. It would be tedious to have to manually define a C++ wrapper function for each and every constructor, so with the [clever use of templates](https://github.com/dtolnay/cxx/issues/280#issuecomment-1344153115) we can define one C++ function which follows a pattern of taking some number of arguments, calling the constructor of `T` with those arguments, and returning a `UniquePtr` to `T`: 47 | 48 | ```c++ 49 | // Generic template constructor 50 | template std::unique_ptr construct_unique(Args... args) { 51 | return std::unique_ptr(new T(args...)); 52 | } 53 | ``` 54 | 55 | Here is an example of binding to the constructor of `BRepPrimAPI_MakeBox`, which is a class which constructs...a box (the physical kind, not the Rust kind). 56 | 57 | ```rust 58 | #[cxx_name = "construct_unique"] 59 | pub fn BRepPrimAPI_MakeBox_ctor( 60 | point: &gp_Pnt, 61 | dx: f64, 62 | dy: f64, 63 | dz: f64, 64 | ) -> UniquePtr; 65 | ``` 66 | 67 | With this declaration, we can bind to a C++ constructor and name it `BRepPrimAPI_MakeBox_ctor` on the Rust side without having to write any extra C++ code. 68 | 69 | ### wrapper.hxx 70 | 71 | Sometimes automatic bindings with cxx just doesn't work out - you could be trying to access a static member of a class, cxx can't see through the polymorphism for a parent class method you're trying to call, or the constructor or function you're trying to call doesn't quite follow the `construct_unique` pattern shown above. 72 | 73 | As a last resort, you can define your own wrapper C++ function to have the type signature and logic that you want. 74 | 75 | Example: The `BRepAdaptor_Curve` class has a `Value()` function which returns `gp_Pnt` directly. Although `gp_Pnt` is a pretty trivial class with XYZ coordinates, we can't return it directly because of the rules imposed by cxx. 76 | 77 | To work around this, I define a Rust function in the cxx bridge like so: 78 | 79 | ```rust 80 | pub fn BRepAdaptor_Curve_value(curve: &BRepAdaptor_Curve, u: f64) -> UniquePtr; 81 | ``` 82 | 83 | Then I add a C++ function with the same name in `wrapper.hxx`: 84 | 85 | ```c++ 86 | inline std::unique_ptr BRepAdaptor_Curve_value(const BRepAdaptor_Curve &curve, const Standard_Real U) { 87 | return std::unique_ptr(new gp_Pnt(curve.Value(U))); 88 | } 89 | ``` 90 | 91 | This could possibly also be solved with a clever C++ template, I'm not sure. 92 | 93 | ## Example: Binding to the STEP File Import Functionality 94 | 95 | In order to give a concrete "tutorial" on binding to new functionality in OpenCascade, I'll go over what was required to add STEP file import functionality to this crate. You can see all the changes for that functionality in [this PR](https://github.com/bschwind/opencascade-rs/pull/33). 96 | 97 | You can see the rough outline of loading a STEP file in native OpenCascade code [here](https://dev.opencascade.org/doc/overview/html/occt_user_guides__step.html): 98 | 99 | ```c++ 100 | STEPControl_Reader reader; 101 | reader.ReadFile("object.step"); 102 | reader.TransferRoots(); 103 | TopoDS_Shape shape = reader.OneShape(); 104 | ``` 105 | 106 | 107 | ### Step 1 - Declare the C++ types and functions in the bridge file 108 | First we need to make the cxx bridge aware of the STEPControl_Reader type, and define a constructor function: 109 | 110 | ```rust 111 | type STEPControl_Reader; 112 | 113 | #[cxx_name = "construct_unique"] 114 | pub fn STEPControl_Reader_ctor() -> UniquePtr; 115 | ``` 116 | 117 | Mercifully, the reader constructor requires zero parameters. But the next step, reading from a file, requires us to pass a String argument for the file name. cxx provides some facilities for converting between Rust strings and C++ strings, but now we'll have to define a function manually in `wrapper.hxx`. 118 | 119 | ```rust 120 | pub fn read_step( 121 | reader: Pin<&mut STEPControl_Reader>, 122 | filename: String, 123 | ) -> IFSelect_ReturnStatus; 124 | ``` 125 | 126 | This returns a `IFSelect_ReturnStatus` so we also need to declare that type in the bridge file: 127 | 128 | ```rust 129 | type IFSelect_ReturnStatus; 130 | ``` 131 | 132 | The cool part here is that `IFSelect_ReturnStatus` is just a simple enum, so we can actually declare a Rust enum of the same name inside of the bridge `ffi` module, but outside of the `unsafe extern "C++" {}` block: 133 | 134 | ```rust 135 | #[derive(Debug)] 136 | #[repr(u32)] 137 | pub enum IFSelect_ReturnStatus { 138 | IFSelect_RetVoid, 139 | IFSelect_RetDone, 140 | IFSelect_RetError, 141 | IFSelect_RetFail, 142 | IFSelect_RetStop, 143 | } 144 | ``` 145 | 146 | This means we can have functions which return `IFSelect_ReturnStatus` directly instead of dealing with it behind a reference or smart pointer. 147 | 148 | ### Step 2 - Write the wrapper C++ code 149 | 150 | The wrapper functions usually end up being trivial, they usually just have an easy-to-bind signature from Rust and then do whatever translation is required. In this case, we're returning an enum that is declared. 151 | 152 | ```c++ 153 | inline IFSelect_ReturnStatus read_step(STEPControl_Reader &reader, rust::String theFileName) { 154 | return reader.ReadFile(theFileName.c_str()); 155 | } 156 | ``` 157 | 158 | ### Step 3 - Include the proper files in wrapper.hxx 159 | 160 | In our case, we need to throw this include at the top of `wrapper.hxx` 161 | 162 | ```c++ 163 | #include 164 | ``` 165 | 166 | ### Step 4 - Declare the rest of the C++ functions 167 | 168 | The next function to bind is `reader.TransferRoots()`. Its C++ definition looks like this: 169 | 170 | ```c++ 171 | Standard_EXPORT Standard_Integer TransferRoots(const Message_ProgressRange& theProgress = Message_ProgressRange()); 172 | ``` 173 | 174 | Luckily we can bind directly to it with Rust: 175 | 176 | ```rust 177 | pub fn TransferRoots( 178 | self: Pin<&mut STEPControl_Reader>, 179 | progress: &Message_ProgressRange, 180 | ) -> i32; 181 | ``` 182 | 183 | Note that `TransferRoots` is not a `const` function so we need to pass `Pin<&mut STEPControl_Reader>`. 184 | 185 | Finally, there is `reader.OneShape()`: 186 | 187 | ```c++ 188 | Standard_EXPORT TopoDS_Shape OneShape() const; 189 | ``` 190 | 191 | Unfortunately, this returns a bare `TopoDS_Shape` so we can't directly bind to it, we'll have to create a wrapper C++ function. 192 | 193 | ```rust 194 | pub fn one_shape_step(reader: &STEPControl_Reader) -> UniquePtr; 195 | ``` 196 | 197 | and 198 | 199 | ```c++ 200 | inline std::unique_ptr one_shape_step(const STEPControl_Reader &reader) { 201 | return std::unique_ptr(new TopoDS_Shape(reader.OneShape())); 202 | } 203 | ``` 204 | 205 | ### Step 5 - Link to the OpenCascade libraries in `build.rs` 206 | 207 | OpenCascade is split up into lots of smaller libraries, and sometimes if you're bringing in new functionality for the first time, you may need to link to a new library. Otherwise you'll run into huge amounts of linker errors if you try to build a binary based on this new code. 208 | 209 | In the case of adding STEP file support, we need to link to five (!) different libraries. We'll add them in `build.rs`: 210 | 211 | ```rust 212 | println!("cargo:rustc-link-lib=static=TKSTEP"); 213 | println!("cargo:rustc-link-lib=static=TKSTEPAttr"); 214 | println!("cargo:rustc-link-lib=static=TKSTEPBase"); 215 | println!("cargo:rustc-link-lib=static=TKSTEP209"); 216 | println!("cargo:rustc-link-lib=static=TKXSBase"); 217 | ``` 218 | 219 | How do you know which libraries to link? If you look up `STEPControl_Reader` you'll likely arrive at [this page](https://dev.opencascade.org/doc/refman/html/class_s_t_e_p_control___reader.html). You can find the library name it's contained in here: 220 | 221 | ![TKSTEP](./images/cascade_library.png) 222 | 223 | The rest of the libraries were found through trial and error. The linker will complain that symbols are missing, so copy-paste those symbols into a search engine, find them on OpenCascade's documentation site, and note which library they came from. Alternatively, you could poke through the project's CMake and other build files to determine which libraries you need to link to. 224 | 225 | ### Step 6 - Give it a nicer Rust API 226 | 227 | Finally, we can create a higher level Rust function which reads a STEP file and returns a `Shape` primitive: 228 | 229 | ```rust 230 | pub fn from_step_file>(path: P) -> Shape { 231 | let mut reader = STEPControl_Reader_ctor(); 232 | let _return_status = 233 | read_step(reader.pin_mut(), path.as_ref().to_string_lossy().to_string()); 234 | reader.pin_mut().TransferRoots(&Message_ProgressRange_ctor()); 235 | 236 | let inner = one_shape_step(&reader); 237 | 238 | // Assuming a Shape struct has a UniquePtr field called `inner` 239 | Shape { inner } 240 | } 241 | ``` 242 | 243 | Of course, a better version would inquire into the STEP translation process and return a `Result` so we can handle the case where something fails. 244 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | clap = { version = "4", features = ["derive"] } 8 | opencascade = { version = "0.2", path = "../crates/opencascade", default-features = false } 9 | glam = { version = "0.24", features = ["bytemuck"] } 10 | 11 | [features] 12 | default = ["builtin"] 13 | builtin = ["opencascade/builtin"] 14 | 15 | [lib] 16 | name = "examples" 17 | path = "src/lib.rs" 18 | 19 | [[bin]] 20 | name = "write_model" 21 | path = "src/write_model.rs" 22 | -------------------------------------------------------------------------------- /examples/src/airfoil.rs: -------------------------------------------------------------------------------- 1 | // This example demonstrates using a bezier surface to create an airfoil like shape 2 | 3 | use glam::dvec3; 4 | use opencascade::primitives::{Face, Shape, Surface}; 5 | 6 | pub fn shape() -> Shape { 7 | let points1 = [ 8 | dvec3(0.0, 0.0, 0.0), 9 | dvec3(5.0, 0.0, 10.0), 10 | dvec3(20.0, 0.0, 10.0), 11 | dvec3(50.0, 0.0, 10.0), 12 | dvec3(100.0, 0.0, 0.0), 13 | ]; 14 | let points2 = [ 15 | dvec3(0.0, 0.0, 0.0), 16 | dvec3(-2.0, 0.0, -8.0), 17 | dvec3(50.0, 0.0, 0.0), 18 | dvec3(50.0, 0.0, 0.0), 19 | dvec3(100.0, 0.0, 0.0), 20 | ]; 21 | 22 | let surface = Surface::bezier([points1, points2]); 23 | let face = Face::from_surface(&surface); 24 | 25 | let airfoil = face.extrude(dvec3(0.0, 50.0, 0.0)); 26 | 27 | airfoil.into() 28 | } 29 | -------------------------------------------------------------------------------- /examples/src/box_shape.rs: -------------------------------------------------------------------------------- 1 | use opencascade::primitives::Shape; 2 | 3 | pub fn shape() -> Shape { 4 | let my_box = Shape::box_with_dimensions(10.0, 10.0, 1.0); 5 | let another_box = Shape::box_with_dimensions(1.0, 1.0, 0.8); 6 | 7 | my_box.subtract(&another_box).chamfer(0.07) 8 | } 9 | -------------------------------------------------------------------------------- /examples/src/cable_bracket.rs: -------------------------------------------------------------------------------- 1 | use glam::{dvec3, DVec3}; 2 | use opencascade::{ 3 | primitives::{Direction, Face, IntoShape, Shape, Wire}, 4 | workplane::Workplane, 5 | }; 6 | use std::f64::consts::PI; 7 | 8 | pub fn shape() -> Shape { 9 | let width = 15.0; 10 | let thickness = 2.5; 11 | let cable_radius = 6.0 / 2.0; 12 | let leg_length = 15.0; 13 | 14 | let pre_bend_radius = thickness; 15 | let bend_start = cable_radius + (thickness / 2.0) + pre_bend_radius; 16 | let max_extent = bend_start + leg_length; 17 | 18 | let face_profile: Face = Workplane::yz() 19 | .translated(DVec3::new(0.0, 0.0, -max_extent)) 20 | .rect(width, thickness) 21 | .to_face(); 22 | 23 | let x = (PI / 4.0).cos() * pre_bend_radius; 24 | let y = (1.0 - (PI / 4.0).sin()) * pre_bend_radius; 25 | 26 | let path: Wire = Workplane::xz() 27 | .sketch() 28 | .move_to(-max_extent, 0.0) 29 | .line_to(-bend_start, 0.0) 30 | .three_point_arc((-bend_start + x, y), (-bend_start + pre_bend_radius, pre_bend_radius)) 31 | .three_point_arc((0.0, bend_start), (bend_start - pre_bend_radius, pre_bend_radius)) 32 | .three_point_arc((bend_start - x, y), (bend_start, 0.0)) 33 | .line_to(max_extent, 0.0) 34 | .wire(); 35 | 36 | let pipe_solid = face_profile.sweep_along(&path).into_shape(); 37 | 38 | // Retrieve the vertical edges on the farthest left and right faces, 39 | // we want to fillet them. 40 | let left_edges = 41 | pipe_solid.faces().farthest(Direction::NegX).edges().parallel_to(Direction::PosZ); 42 | 43 | let right_edges = 44 | pipe_solid.faces().farthest(Direction::PosX).edges().parallel_to(Direction::PosZ); 45 | 46 | let mut bracket = 47 | pipe_solid.fillet_edges(width / 2.5, left_edges.chain(right_edges)).fillet(1.0); 48 | 49 | let drill_point = bend_start + (leg_length / 2.0); 50 | 51 | let indentation_height = thickness - 1.63; 52 | let thumbtack_pin_radius = 3.15 / 2.0; 53 | let thumbtack_base_radius = 10.1 / 2.0; 54 | 55 | for x_pos in [drill_point, -drill_point] { 56 | let cylinder = Shape::cylinder( 57 | dvec3(x_pos, 0.0, (thickness / 2.0) - indentation_height), 58 | thumbtack_base_radius, 59 | DVec3::Z, 60 | 3.0, 61 | ); 62 | 63 | bracket = bracket.subtract(&cylinder).chamfer_new_edges(0.3); 64 | } 65 | 66 | for x_pos in [drill_point, -drill_point] { 67 | bracket = bracket.drill_hole(dvec3(-x_pos, 0.0, 0.0), DVec3::Z, thumbtack_pin_radius); 68 | } 69 | 70 | bracket 71 | } 72 | -------------------------------------------------------------------------------- /examples/src/chamfer.rs: -------------------------------------------------------------------------------- 1 | use glam::dvec3; 2 | use opencascade::{ 3 | primitives::{Direction, Face, Shape, Solid}, 4 | workplane::Workplane, 5 | }; 6 | 7 | pub fn shape() -> Shape { 8 | // A tapering chamfer from bottom to top 2->1 9 | let base = Workplane::xy().rect(10.0, 10.0).chamfer(2.0); 10 | let top = Workplane::xy().rect(10.0, 10.0).translate(dvec3(0.0, 0.0, 10.0)).chamfer(1.0); 11 | 12 | let chamfered_box = Solid::loft([&base, &top]); 13 | 14 | // Insert the workplane into the chamfered box area so union returns edges 15 | let handle = Workplane::xy().translated(dvec3(0.0, 0.0, 0.1)).rect(5.0, 5.0); 16 | let handle_face = Face::from_wire(&handle); 17 | 18 | let handle_body = handle_face.extrude(dvec3(0.0, 0.0, -10.1)); 19 | let chamfered_shape = chamfered_box.union(&handle_body).chamfer_new_edges(0.5); 20 | 21 | // Chamfer the top of the protrusion 22 | let top_edges = chamfered_shape 23 | .faces() 24 | .farthest(Direction::NegZ) // Get the face whose center of mass is the farthest in the negative Z direction 25 | .edges(); // Get all the edges of this face 26 | 27 | chamfered_shape.chamfer_edges(1.0, top_edges) 28 | 29 | // Can also just chamfer the whole shape with: 30 | // chamfered_shape.chamfer(0.5) 31 | } 32 | -------------------------------------------------------------------------------- /examples/src/flat_ethernet_bracket.rs: -------------------------------------------------------------------------------- 1 | use glam::{dvec3, DVec3}; 2 | use opencascade::{ 3 | primitives::{Direction, IntoShape, Shape, Wire}, 4 | workplane::Workplane, 5 | }; 6 | 7 | pub fn shape() -> Shape { 8 | let thumbtack_big_diameter = 10.75; 9 | let thumbtack_big_radius = thumbtack_big_diameter / 2.0; 10 | let thumbtack_pin_radius = 1.2 / 2.0; 11 | let thickness = 1.5; 12 | let cable_width = 7.6; 13 | let cable_height = 2.3; 14 | 15 | let overhang_width = 1.0; 16 | 17 | let path: Wire = Workplane::xz() 18 | .sketch() 19 | .line_dx(thickness) 20 | .line_dy(-(thumbtack_big_diameter + cable_width + 2.0)) 21 | .line_dx(cable_height + 0.1) 22 | .line_dy(cable_width + 0.1) 23 | .line_dx(-overhang_width) 24 | .line_dy(thickness) 25 | .line_dx(overhang_width + thickness) 26 | .line_dy(-(thickness + cable_width + thickness)) 27 | .line_dx(-(thickness + cable_height + thickness)) 28 | .close(); 29 | 30 | let mut bracket = path.to_face().extrude(dvec3(0.0, thumbtack_big_diameter, 0.0)).into_shape(); 31 | 32 | let top_edges = bracket.faces().farthest(Direction::PosZ).edges().parallel_to(Direction::PosX); 33 | 34 | bracket = bracket.fillet_edges(thumbtack_big_diameter / 2.1, top_edges); 35 | 36 | let cylinder = Shape::cylinder( 37 | dvec3(thickness, thumbtack_big_radius, -thumbtack_big_radius), 38 | thumbtack_big_radius + 0.2, 39 | -DVec3::X, 40 | thickness - 1.0, 41 | ); 42 | 43 | bracket = bracket.subtract(&cylinder).fillet(0.2); 44 | 45 | bracket = bracket.drill_hole( 46 | dvec3(thickness, thumbtack_big_radius, -thumbtack_big_radius), 47 | -DVec3::X, 48 | thumbtack_pin_radius, 49 | ); 50 | 51 | bracket 52 | } 53 | -------------------------------------------------------------------------------- /examples/src/gizmo.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::{ 3 | primitives::{Shape, Solid}, 4 | workplane::Workplane, 5 | }; 6 | 7 | pub fn shape() -> Shape { 8 | let arrow_length = 10.0; 9 | let cone_height = 2.0; 10 | let shaft_length = arrow_length - cone_height; 11 | 12 | let arrow = |workplane: Workplane| { 13 | let shaft = 14 | workplane.circle(0.0, 0.0, 0.1).to_face().extrude(workplane.normal() * arrow_length); 15 | let cone_base = 16 | workplane.translated(DVec3::new(0.0, 0.0, shaft_length)).circle(0.0, 0.0, 1.0); 17 | let cone_top = 18 | workplane.translated(DVec3::new(0.0, 0.0, arrow_length)).circle(0.0, 0.0, 0.05); 19 | let cone = Solid::loft([&cone_base, &cone_top]); 20 | let arrow_shape = shaft.union(&cone); 21 | 22 | arrow_shape.shape 23 | }; 24 | 25 | arrow(Workplane::yz()).union(&arrow(Workplane::xz())).union(&arrow(Workplane::xy())).shape 26 | } 27 | -------------------------------------------------------------------------------- /examples/src/heater_coil.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::{ 3 | angle::{RVec, ToAngle}, 4 | primitives::{Edge, IntoShape, Shape, Wire}, 5 | workplane::Workplane, 6 | }; 7 | 8 | pub fn shape() -> Shape { 9 | let a = 1.0; 10 | let spiral_radius = 5.0; 11 | let spiral_pitch = 3.0; 12 | let spiral_half_turn_count = 5; 13 | let attach_len = 4.0; 14 | 15 | let face_profile = Workplane::xz() 16 | .translated(DVec3::new(spiral_radius, 0.0, attach_len)) 17 | .rotated(RVec::z(45.0.degrees())) 18 | .rect(a, a) 19 | .to_face(); 20 | 21 | let sample_count = 20; 22 | let spiral_points: Vec = (0..sample_count) 23 | .map(|i| { 24 | let t = i as f64 / (sample_count - 1) as f64; 25 | let angle_rad = spiral_half_turn_count as f64 * std::f64::consts::PI * t; 26 | let (y, x) = angle_rad.sin_cos(); 27 | 28 | let u = 0.5 * spiral_half_turn_count as f64 * t; 29 | let z = spiral_pitch * u; 30 | 31 | DVec3::new(spiral_radius * x, spiral_radius * y, z) 32 | }) 33 | .collect(); 34 | 35 | let p0 = spiral_points[0]; 36 | let p1 = spiral_points[sample_count - 1]; 37 | 38 | let coil = Edge::spline_from_points(spiral_points, None); 39 | let attach_0 = Edge::segment(p0 - DVec3::new(0.0, attach_len, 0.0), p0); 40 | let attach_1 = Edge::segment(p1, p1 - DVec3::new(0.0, attach_len, 0.0)); 41 | let path = Wire::from_edges(&[attach_0, coil, attach_1]); 42 | 43 | let pipe_solid = face_profile.sweep_along(&path); 44 | pipe_solid.into_shape() 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/high_level_bottle.rs: -------------------------------------------------------------------------------- 1 | use glam::{dvec3, DVec3}; 2 | use opencascade::primitives::{Direction::PosZ, Edge, Face, IntoShape, Shape, Wire}; 3 | 4 | pub fn shape() -> Shape { 5 | let height = 70.0; 6 | let width = 50.0; 7 | let thickness = 30.0; 8 | 9 | // Define the points making up the bottle's profile. 10 | let point_1 = dvec3(-width / 2.0, 0.0, 0.0); 11 | let point_2 = dvec3(-width / 2.0, -thickness / 4.0, 0.0); 12 | let point_3 = dvec3(0.0, -thickness / 2.0, 0.0); 13 | let point_4 = dvec3(width / 2.0, -thickness / 4.0, 0.0); 14 | let point_5 = dvec3(width / 2.0, 0.0, 0.0); 15 | 16 | let arc = Edge::arc(point_2, point_3, point_4); 17 | let segment_1 = Edge::segment(point_1, point_2); 18 | let segment_2 = Edge::segment(point_4, point_5); 19 | 20 | let wire = Wire::from_edges([&segment_1, &arc, &segment_2]); 21 | let mirrored_wire = wire.mirror_along_axis(dvec3(0.0, 0.0, 0.0), dvec3(1.0, 0.0, 0.0)); 22 | 23 | let wire_profile = Wire::from_wires([&wire, &mirrored_wire]); 24 | let face_profile = Face::from_wire(&wire_profile); 25 | 26 | let body = face_profile.extrude(dvec3(0.0, 0.0, height)).into_shape().fillet(thickness / 12.0); 27 | 28 | // Create the neck and join it with the body 29 | let neck_radius = thickness / 4.0; 30 | let neck_height = height / 10.0; 31 | 32 | let neck = Shape::cylinder(dvec3(0.0, 0.0, height), neck_radius, DVec3::Z, neck_height); 33 | let bottle = neck.union(&body); 34 | 35 | let top_face = bottle.faces().farthest(PosZ); 36 | bottle.hollow(-thickness / 50.0, [top_face]) 37 | } 38 | -------------------------------------------------------------------------------- /examples/src/keycap.rs: -------------------------------------------------------------------------------- 1 | // Keycap generator, referenced from 2 | // https://github.com/cubiq/OPK/blob/53f9d6a4123b0f309f87158115c83d19811b3484/opk.py 3 | use glam::dvec3; 4 | use opencascade::{ 5 | angle::{RVec, ToAngle}, 6 | primitives::{Direction, Face, Shape, Solid}, 7 | workplane::Workplane, 8 | }; 9 | 10 | const KEYCAP_PITCH: f64 = 19.05; 11 | 12 | pub fn shape() -> Shape { 13 | let convex = false; 14 | let keycap_unit_size_x = 1.0; 15 | let keycap_unit_size_y = 1.0; 16 | let height = 16.0; 17 | let angle = 13.0.degrees(); 18 | let depth: f64 = 2.8; 19 | let thickness: f64 = 1.5; 20 | let base = 18.2; 21 | let top = 13.2; 22 | let curve = 1.7; 23 | let bottom_fillet = 0.5; 24 | let top_fillet = 5.0; 25 | let tension = if convex { 0.4 } else { 1.0 }; 26 | let pos = false; // Use POS-style stabilizers 27 | 28 | let top_diff = base - top; 29 | 30 | let bx = KEYCAP_PITCH * keycap_unit_size_x - (KEYCAP_PITCH - base); 31 | let by = KEYCAP_PITCH * keycap_unit_size_y - (KEYCAP_PITCH - base); 32 | 33 | let tx = bx - top_diff; 34 | let ty = by - top_diff; 35 | 36 | let base = Workplane::xy().rect(bx, by).fillet(bottom_fillet); 37 | 38 | let mid = Workplane::xy().rect(bx, by).fillet((top_fillet - bottom_fillet) / 3.0).transform( 39 | dvec3(0.0, 0.0, height / 4.0), 40 | dvec3(1.0, 0.0, 0.0), 41 | angle / 4.0, 42 | ); 43 | 44 | // We should use `ConnectEdgesToWires` for `Wire::from_edges`, as it 45 | // likely puts these arcs in the order we want. 46 | let top_wire = Workplane::xy() 47 | .sketch() 48 | .arc((curve, curve * tension), (0.0, ty / 2.0), (curve, ty - curve * tension)) 49 | .arc((curve, ty - curve * tension), (tx / 2.0, ty), (tx - curve, ty - curve * tension)) 50 | .arc((tx - curve, ty - curve * tension), (tx, ty / 2.0), (tx - curve, curve * tension)) 51 | .arc((tx - curve, curve * tension), (tx / 2.0, 0.0), (curve, curve * tension)) 52 | .wire() 53 | .fillet(top_fillet) 54 | .translate(dvec3(-tx / 2.0, -ty / 2.0, 0.0)) 55 | .transform(dvec3(0.0, 0.0, height), dvec3(1.0, 0.0, 0.0), angle); 56 | 57 | let keycap = Solid::loft([&base, &mid, &top_wire]); 58 | 59 | let scoop = if convex { 60 | let scoop = Workplane::yz() 61 | .transformed(dvec3(0.0, height - 2.1, -bx / 2.0), RVec::z(angle)) 62 | .sketch() 63 | .move_to(-by / 2.0, -1.0) 64 | .three_point_arc((0.0, 2.0), (by / 2.0, -1.0)) 65 | .line_to(by / 2.0, 10.0) 66 | .line_to(-by / 2.0, 10.0) 67 | .close(); 68 | 69 | let scoop = Face::from_wire(&scoop); 70 | scoop.extrude(dvec3(bx, 0.0, 0.0)) 71 | } else { 72 | let scoop_right = Workplane::yz() 73 | .transformed(dvec3(0.0, height, bx / 2.0), RVec::z(angle)) 74 | .sketch() 75 | .move_to(-by / 2.0 + 2.0, 0.0) 76 | .three_point_arc((0.0, (-depth + 1.5).min(-0.1)), (by / 2.0 - 2.0, 0.0)) 77 | .line_to(by / 2.0, height) 78 | .line_to(-by / 2.0, height) 79 | .close(); 80 | 81 | let scoop_mid = Workplane::yz() 82 | .transformed(dvec3(0.0, height, 0.0), RVec::z(angle)) 83 | .sketch() 84 | .move_to(-by / 2.0 - 2.0, -0.5) 85 | .three_point_arc((0.0, -depth), (by / 2.0 + 2.0, -0.5)) 86 | .line_to(by / 2.0, height) 87 | .line_to(-by / 2.0, height) 88 | .close(); 89 | 90 | let scoop_left = Workplane::yz() 91 | .transformed(dvec3(0.0, height, -bx / 2.0), RVec::z(angle)) 92 | .sketch() 93 | .move_to(-by / 2.0 + 2.0, 0.0) 94 | .three_point_arc((0.0, (-depth + 1.5).min(-0.1)), (by / 2.0 - 2.0, 0.0)) 95 | .line_to(by / 2.0, height) 96 | .line_to(-by / 2.0, height) 97 | .close(); 98 | 99 | Solid::loft([&scoop_right, &scoop_mid, &scoop_left]) 100 | }; 101 | 102 | let keycap = keycap.subtract(&scoop).fillet_new_edges(0.6); 103 | 104 | let shell_bottom = Workplane::xy().rect(bx - thickness * 2.0, by - thickness * 2.0); 105 | 106 | let shell_mid = Workplane::xy() 107 | .translated(dvec3(0.0, 0.0, height / 4.0)) 108 | .rect(bx - thickness * 3.0, by - thickness * 3.0); 109 | 110 | let shell_top = Workplane::xy() 111 | .transformed(dvec3(0.0, 0.0, (height / 4.0) + height - height / 4.0 - 4.5), RVec::x(angle)) 112 | .rect(tx - thickness * 2.0 + 0.5, ty - thickness * 2.0 + 0.5); 113 | 114 | let shell: Shape = Solid::loft([&shell_bottom, &shell_mid, &shell_top]).into(); 115 | 116 | let mut keycap = keycap.subtract(&shell); 117 | 118 | let temp_face = 119 | shell.faces().farthest(Direction::PosZ).workplane().rect(bx * 2.0, by * 2.0).to_face(); 120 | 121 | let mut stem_points = vec![]; 122 | let mut ribh_points = vec![]; 123 | let mut ribv_points = vec![]; 124 | 125 | if pos { 126 | let stem_num_x = keycap_unit_size_x.floor(); 127 | let stem_num_y = keycap_unit_size_y.floor(); 128 | 129 | let stem_start_x = round_digits(-KEYCAP_PITCH * (stem_num_x / 2.0) + KEYCAP_PITCH / 2.0, 6); 130 | let stem_start_y = round_digits(-KEYCAP_PITCH * (stem_num_y / 2.0) + KEYCAP_PITCH / 2.0, 6); 131 | 132 | for i in 0..(stem_num_y as usize) { 133 | ribh_points.push((0.0, stem_start_y + i as f64 * KEYCAP_PITCH)); 134 | 135 | for l in 0..(stem_num_x as usize) { 136 | if i == 0 { 137 | ribv_points.push((stem_start_x + l as f64 * KEYCAP_PITCH, 0.0)); 138 | } 139 | 140 | stem_points.push(( 141 | stem_start_x + l as f64 * KEYCAP_PITCH, 142 | stem_start_y + i as f64 * KEYCAP_PITCH, 143 | )); 144 | } 145 | } 146 | } else { 147 | stem_points.push((0.0, 0.0)); 148 | 149 | if keycap_unit_size_y > keycap_unit_size_x { 150 | if keycap_unit_size_y > 2.75 { 151 | let dist = keycap_unit_size_y / 2.0 * KEYCAP_PITCH - KEYCAP_PITCH / 2.0; 152 | stem_points.extend_from_slice(&[(0.0, dist), (0.0, -dist)]); 153 | } else if keycap_unit_size_y > 1.75 { 154 | let dist = 2.25 / 2.0 * KEYCAP_PITCH - KEYCAP_PITCH / 2.0; 155 | stem_points.extend_from_slice(&[(0.0, -dist), (0.0, dist)]); 156 | } 157 | 158 | ribh_points.clone_from(&stem_points); 159 | ribv_points.push((0.0, 0.0)); 160 | } else { 161 | if keycap_unit_size_x > 2.75 { 162 | let dist = keycap_unit_size_x / 2.0 * KEYCAP_PITCH - KEYCAP_PITCH / 2.0; 163 | stem_points.extend_from_slice(&[(dist, 0.0), (-dist, 0.0)]); 164 | } else if keycap_unit_size_x > 1.75 { 165 | let dist = 2.25 / 2.0 * KEYCAP_PITCH - KEYCAP_PITCH / 2.0; 166 | stem_points.extend_from_slice(&[(dist, 0.0), (-dist, 0.0)]); 167 | } 168 | 169 | ribh_points.push((0.0, 0.0)); 170 | ribv_points.clone_from(&stem_points); 171 | } 172 | } 173 | 174 | let bottom_face = keycap.faces().farthest(Direction::NegZ); 175 | 176 | let bottom_workplane = bottom_face.workplane().translated(dvec3(0.0, 0.0, -4.5)); 177 | 178 | for (x, y) in &stem_points { 179 | let circle = bottom_workplane.circle(*x, *y, 2.75).to_face(); 180 | 181 | let post = circle.extrude_to_face(&keycap, &temp_face); 182 | 183 | keycap = keycap.union(&post); 184 | } 185 | 186 | for (x, y) in ribh_points { 187 | let rect = bottom_workplane.translated(dvec3(x, y, 0.0)).rect(tx, 0.8).to_face(); 188 | 189 | let rib = rect.extrude_to_face(&keycap, &temp_face); 190 | 191 | keycap = keycap.union(&rib); 192 | } 193 | 194 | for (x, y) in ribv_points { 195 | let rect = bottom_workplane.translated(dvec3(x, y, 0.0)).rect(0.8, ty).to_face(); 196 | 197 | let rib = rect.extrude_to_face(&keycap, &temp_face); 198 | 199 | keycap = keycap.union(&rib); 200 | } 201 | 202 | // TODO(bschwind) - This should probably be done after every union... 203 | let mut keycap = keycap.clean(); 204 | 205 | for (x, y) in &stem_points { 206 | let bottom_face = keycap.faces().farthest(Direction::NegZ); 207 | let workplane = bottom_face.workplane().translated(dvec3(0.0, 0.0, -0.6)); 208 | 209 | let circle = workplane.circle(*x, *y, 2.75).to_face(); 210 | 211 | // TODO(bschwind) - Abstract all this into a "extrude_to_next_face" function. 212 | let origin = workplane.to_world_pos(dvec3(*x, *y, 0.0)); 213 | let faces = keycap.faces_along_line(origin, workplane.normal()); 214 | let nearest_hit = faces 215 | .iter() 216 | .min_by(|hit_a, hit_b| hit_a.t.partial_cmp(&hit_b.t).unwrap()) 217 | .expect("We should have a face to extrude to"); 218 | let post = circle.extrude_to_face(&keycap, &nearest_hit.face); 219 | 220 | keycap = keycap.union(&post).into(); 221 | } 222 | 223 | let r1 = Face::from_wire(&Workplane::xy().rect(4.15, 1.27)); 224 | let r2 = Face::from_wire(&Workplane::xy().rect(1.27, 4.15)); 225 | 226 | let mut cross = r1.union(&r2).clean(); 227 | 228 | for (x, y) in stem_points { 229 | cross.set_global_translation(dvec3(x, y, 0.0)); 230 | let cross = cross.extrude(dvec3(0.0, 0.0, 4.6)); 231 | 232 | keycap = keycap.subtract(&cross).chamfer_new_edges(0.2); 233 | } 234 | 235 | keycap 236 | } 237 | 238 | fn round_digits(num: f64, digits: i32) -> f64 { 239 | let multiplier = 10.0f64.powi(digits); 240 | (num * multiplier).round() / multiplier 241 | } 242 | -------------------------------------------------------------------------------- /examples/src/letter_a.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::primitives::{Edge, Face, Shape, Wire}; 3 | 4 | type Contour = Vec>; 5 | 6 | pub fn shape() -> Shape { 7 | let outer = contour_to_face(outer_contour()); 8 | let inner = contour_to_face(inner_contour()); 9 | outer.subtract(&inner).extrude(DVec3::new(0.0, 0.0, 10.0)) 10 | } 11 | 12 | const SCALE_FACTOR: f64 = 1.0 / 16.0; 13 | const CENTER_X: i32 = 256; 14 | const CENTER_Y: i32 = 256; 15 | 16 | fn contour_to_face(contour: Contour) -> Face { 17 | let edges: Vec = contour 18 | .into_iter() 19 | .map(|segment_points| { 20 | let points = segment_points.into_iter().map(|(x, y)| { 21 | let x = (x - CENTER_X) as f64 * SCALE_FACTOR; 22 | let y = (y - CENTER_Y) as f64 * SCALE_FACTOR; 23 | DVec3::new(x, y, 0.0) 24 | }); 25 | Edge::bezier(points) 26 | }) 27 | .collect(); 28 | let wire = Wire::from_edges(&edges); 29 | Face::from_wire(&wire) 30 | } 31 | 32 | fn outer_contour() -> Contour { 33 | vec![ 34 | vec![(450, 123), (450, 91), (472, 60), (494, 52)], 35 | vec![(494, 52), (473, -12)], 36 | vec![(473, -12), (432, -7), (407, 11)], 37 | vec![(407, 11), (382, 29), (370, 67)], 38 | vec![(370, 67), (317, -12), (213, -12)], 39 | vec![(213, -12), (135, -12), (90, 32)], 40 | vec![(90, 32), (45, 76), (45, 147)], 41 | vec![(45, 147), (45, 231), (105, 276)], 42 | vec![(105, 276), (166, 321), (277, 321)], 43 | vec![(277, 321), (358, 321)], 44 | vec![(358, 321), (358, 360)], 45 | vec![(358, 360), (358, 416), (331, 440)], 46 | vec![(331, 440), (304, 464), (248, 464)], 47 | vec![(248, 464), (190, 464), (106, 436)], 48 | vec![(106, 436), (83, 503)], 49 | vec![(83, 503), (181, 539), (265, 539)], 50 | vec![(265, 539), (358, 539), (404, 493)], 51 | vec![(404, 493), (450, 448), (450, 364)], 52 | vec![(450, 364), (450, 123)], 53 | ] 54 | } 55 | 56 | fn inner_contour() -> Contour { 57 | vec![ 58 | vec![(234, 57), (313, 57), (358, 139)], 59 | vec![(358, 139), (358, 260)], 60 | vec![(358, 260), (289, 260)], 61 | vec![(289, 260), (143, 260), (143, 152)], 62 | vec![(143, 152), (143, 105), (166, 81)], 63 | vec![(166, 81), (189, 57), (234, 57)], 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use opencascade::primitives::Shape; 3 | 4 | pub mod airfoil; 5 | pub mod box_shape; 6 | pub mod cable_bracket; 7 | pub mod chamfer; 8 | pub mod flat_ethernet_bracket; 9 | pub mod gizmo; 10 | pub mod heater_coil; 11 | pub mod high_level_bottle; 12 | pub mod keyboard_case; 13 | pub mod keycap; 14 | pub mod letter_a; 15 | pub mod offset_2d; 16 | pub mod rounded_chamfer; 17 | pub mod section; 18 | pub mod swept_face; 19 | pub mod swept_face_variable; 20 | pub mod swept_wire; 21 | pub mod swept_wire_variable; 22 | pub mod turners_cube; 23 | pub mod variable_fillet; 24 | pub mod zbox_case; 25 | 26 | #[derive(Debug, Copy, Clone, PartialEq, ValueEnum)] 27 | pub enum Example { 28 | Airfoil, 29 | CableBracket, 30 | BoxShape, 31 | Chamfer, 32 | FlatEthernetBracket, 33 | Gizmo, 34 | HeaterCoil, 35 | HighLevelBottle, 36 | KeyboardCase, 37 | Keycap, 38 | LetterA, 39 | Offset2d, 40 | RoundedChamfer, 41 | Section, 42 | SweptFace, 43 | SweptFaceVariable, 44 | SweptWire, 45 | SweptWireVariable, 46 | TurnersCube, 47 | VariableFillet, 48 | ZboxCase, 49 | } 50 | 51 | impl Example { 52 | pub fn shape(self) -> Shape { 53 | match self { 54 | Example::Airfoil => airfoil::shape(), 55 | Example::CableBracket => cable_bracket::shape(), 56 | Example::BoxShape => box_shape::shape(), 57 | Example::Chamfer => chamfer::shape(), 58 | Example::FlatEthernetBracket => flat_ethernet_bracket::shape(), 59 | Example::Gizmo => gizmo::shape(), 60 | Example::HeaterCoil => heater_coil::shape(), 61 | Example::HighLevelBottle => high_level_bottle::shape(), 62 | Example::KeyboardCase => keyboard_case::shape(), 63 | Example::Keycap => keycap::shape(), 64 | Example::LetterA => letter_a::shape(), 65 | Example::Offset2d => offset_2d::shape(), 66 | Example::RoundedChamfer => rounded_chamfer::shape(), 67 | Example::Section => section::shape(), 68 | Example::SweptFace => swept_face::shape(), 69 | Example::SweptFaceVariable => swept_face_variable::shape(), 70 | Example::SweptWire => swept_wire::shape(), 71 | Example::SweptWireVariable => swept_wire_variable::shape(), 72 | Example::TurnersCube => turners_cube::shape(), 73 | Example::VariableFillet => variable_fillet::shape(), 74 | Example::ZboxCase => zbox_case::shape(), 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/src/offset_2d.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::{ 3 | primitives::{IntoShape, JoinType, Shape}, 4 | workplane::Workplane, 5 | }; 6 | 7 | // Demonstrates ofsetting a face or wire in 2D. 8 | 9 | pub fn shape() -> Shape { 10 | let mut shapes = vec![]; 11 | 12 | for (i, join_type) in [JoinType::Arc, JoinType::Intersection].into_iter().enumerate() { 13 | let solid = Workplane::xy() 14 | .translated(DVec3::new(32.0 * i as f64, 0.0, 0.0)) 15 | .sketch() 16 | .move_to(0.0, 10.0) 17 | .line_to(5.0, 5.0) 18 | .line_to(5.0, -5.0) 19 | .line_to(0.0, 0.0) 20 | .line_to(-5.0, -5.0) 21 | .line_to(-5.0, 5.0) 22 | .close() 23 | .offset(1.0, join_type) // offset a wire 24 | .to_face() 25 | .extrude(DVec3::new(0.0, 0.0, 8.0)) 26 | .into_shape(); 27 | 28 | shapes.push(solid); 29 | } 30 | 31 | shapes.into_iter().reduce(|acc, item| acc.union(&item).shape).unwrap() 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/rounded_chamfer.rs: -------------------------------------------------------------------------------- 1 | use glam::dvec3; 2 | use opencascade::{ 3 | primitives::{Direction, IntoShape, Shape}, 4 | workplane::Workplane, 5 | }; 6 | 7 | // Demonstrates filleting a 2D profile, extruding it, then chamfering 8 | // the top edges, resulting in a nice, rounded chamfer. 9 | 10 | pub fn shape() -> Shape { 11 | let shape = Workplane::xy() 12 | .rect(16.0, 10.0) 13 | .fillet(1.0) 14 | .to_face() 15 | .extrude(dvec3(0.0, 0.0, 3.0)) 16 | .into_shape(); 17 | 18 | let top_edges = shape.faces().farthest(Direction::PosZ).edges(); 19 | 20 | shape.chamfer_edges(0.7, top_edges) 21 | } 22 | -------------------------------------------------------------------------------- /examples/src/section.rs: -------------------------------------------------------------------------------- 1 | use core::f64; 2 | use glam::dvec3; 3 | use opencascade::{ 4 | angle::Angle::Radians, 5 | primitives::{Compound, IntoShape, Shape}, 6 | section, 7 | workplane::Workplane, 8 | }; 9 | 10 | pub fn shape() -> Shape { 11 | // Create a wire by sketching two arcs on the XY plane 12 | let s = Workplane::xy() 13 | .sketch() 14 | .arc((0.0, -2.0), (1.0, -1.0), (0.0, 0.0)) 15 | .arc((0.0, 0.0), (-1.0, 1.0), (0.0, 2.0)) 16 | .wire(); 17 | 18 | // Create a circular face on the YZ plane 19 | let f = Workplane::yz().circle(0.0, 0.0, 0.5).to_face(); 20 | 21 | // Sweep the circular face along the wire to create a 3D shape 22 | let shape = f.sweep_along(&s).into_shape(); 23 | 24 | // Create a cutting plane and rotate 25 | let p = Workplane::yz() 26 | .rect(10.0, 10.0) 27 | .transform(dvec3(0.0, 0.0, 0.0), dvec3(0.0, 0.0, 1.0), Radians(f64::consts::PI / 8.0)) 28 | .to_face() 29 | .into_shape(); 30 | 31 | // Compute the intersection edges between the swept shape and the transformed rectangle 32 | let edges = section::edges(&shape, &p); 33 | 34 | // Combine the intersection edges, the swept shape, and the rectangle's edges into a compound shape 35 | let all_shapes = [ 36 | edges, // Section edges 37 | vec![shape], // The shape, run with this line commented out 38 | p.edges().map(|e| e.into_shape()).collect(), // The edges of the cutting plane 39 | ]; 40 | 41 | Compound::from_shapes(all_shapes.iter().flatten()).into_shape() 42 | } 43 | -------------------------------------------------------------------------------- /examples/src/swept_face.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::{ 3 | angle::{RVec, ToAngle}, 4 | primitives::{Face, IntoShape, Shape, Solid, Wire}, 5 | workplane::Workplane, 6 | }; 7 | 8 | pub fn shape() -> Shape { 9 | let r = 10.0; 10 | let a = 5.0; 11 | 12 | let face_profile: Face = Workplane::xz() 13 | .rotated(RVec::z(45.0.degrees())) 14 | .translated(DVec3::new(-r, 0.0, 0.0)) 15 | .rect(a, a) 16 | .to_face(); 17 | 18 | let path: Wire = Workplane::xy().sketch().arc((-r, 0.0), (0.0, r), (r, 0.0)).wire(); 19 | 20 | let pipe_solid: Solid = face_profile.sweep_along(&path); 21 | 22 | pipe_solid.into_shape() 23 | } 24 | -------------------------------------------------------------------------------- /examples/src/swept_face_variable.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::{ 3 | angle::{RVec, ToAngle}, 4 | primitives::{approximate_function, Face, IntoShape, Shape, Solid, Wire}, 5 | workplane::Workplane, 6 | }; 7 | 8 | pub fn shape() -> Shape { 9 | let r = 10.0; 10 | let a = 5.0; 11 | 12 | let face_profile: Face = Workplane::xz() 13 | .translated(DVec3::new(-r, 0.0, 0.0)) 14 | .rotated(RVec::z(45.0.degrees())) 15 | .rect(a, a) 16 | .to_face(); 17 | 18 | let path: Wire = Workplane::xy().sketch().arc((-r, 0.0), (0.0, r), (r, 0.0)).wire(); 19 | 20 | let num_radii = 32; 21 | let pipe_solid: Solid = face_profile.sweep_along_with_radius_values( 22 | &path, 23 | approximate_function(num_radii, |t| (-(8.0 * std::f64::consts::PI * t).cos() + 3.0) / 4.0), 24 | ); 25 | 26 | pipe_solid.into_shape() 27 | } 28 | -------------------------------------------------------------------------------- /examples/src/swept_wire.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::{ 3 | angle::{RVec, ToAngle}, 4 | primitives::{IntoShape, Shape, Shell, Wire}, 5 | workplane::Workplane, 6 | }; 7 | 8 | pub fn shape() -> Shape { 9 | let r = 10.0; 10 | let a = 5.0; 11 | 12 | let wire_profile: Wire = Workplane::xz() 13 | .rotated(RVec::z(45.0.degrees())) 14 | .translated(DVec3::new(-r, 0.0, 0.0)) 15 | .rect(a, a); 16 | 17 | let path: Wire = Workplane::xy().sketch().arc((-r, 0.0), (0.0, r), (r, 0.0)).wire(); 18 | 19 | let pipe_shell: Shell = wire_profile.sweep_along(&path); 20 | 21 | pipe_shell.into_shape() 22 | } 23 | -------------------------------------------------------------------------------- /examples/src/swept_wire_variable.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::{ 3 | angle::{RVec, ToAngle}, 4 | primitives::{approximate_function, IntoShape, Shape, Shell, Wire}, 5 | workplane::Workplane, 6 | }; 7 | 8 | pub fn shape() -> Shape { 9 | let r = 10.0; 10 | let a = 5.0; 11 | 12 | let wire_profile: Wire = Workplane::xz() 13 | .translated(DVec3::new(-r, 0.0, 0.0)) 14 | .rotated(RVec::z(45.0.degrees())) 15 | .rect(a, a); 16 | 17 | let path: Wire = Workplane::xy().sketch().arc((-r, 0.0), (0.0, r), (r, 0.0)).wire(); 18 | 19 | let num_radii = 32; 20 | let pipe_shell: Shell = wire_profile.sweep_along_with_radius_values( 21 | &path, 22 | approximate_function(num_radii, |t| (-(8.0 * std::f64::consts::PI * t).cos() + 3.0) / 4.0), 23 | ); 24 | 25 | pipe_shell.into_shape() 26 | } 27 | -------------------------------------------------------------------------------- /examples/src/turners_cube.rs: -------------------------------------------------------------------------------- 1 | use glam::DVec3; 2 | use opencascade::{ 3 | primitives::{IntoShape, Shape}, 4 | workplane::Workplane, 5 | }; 6 | 7 | pub fn shape() -> Shape { 8 | let edge_length_1 = 48.0; 9 | let hole_diamet_1 = 23.0; 10 | let wall_thickn_1 = 5.0; 11 | 12 | let edge_length_2 = edge_len_from_circum_diam(edge_length_1 - 2.0 * wall_thickn_1); 13 | let hole_diamet_2 = 11.0; 14 | let wall_thickn_2 = 4.0; 15 | 16 | let edge_length_3 = edge_len_from_circum_diam(edge_length_2 - 2.0 * wall_thickn_2); 17 | 18 | let c1 = hollow_cube(edge_length_1, hole_diamet_1, wall_thickn_1); 19 | let c2 = hollow_cube(edge_length_2, hole_diamet_2, wall_thickn_2); 20 | let c3 = cube(edge_length_3); 21 | 22 | c1.union(&c2).union(&c3).into_shape() 23 | } 24 | 25 | fn edge_len_from_circum_diam(d: f64) -> f64 { 26 | d / f64::sqrt(2.0) 27 | } 28 | 29 | fn hollow_cube(edge_length: f64, hole_diameter: f64, wall_thickness: f64) -> Shape { 30 | let r1 = 0.5 * hole_diameter; 31 | let h1 = edge_length; 32 | let r2 = 0.5 * edge_length - wall_thickness; 33 | let h2 = edge_length - 2.0 * wall_thickness; 34 | 35 | let p1 = rolling_pin(&Workplane::xy(), r1, h1, r2, h2); 36 | let p2 = rolling_pin(&Workplane::yz(), r1, h1, r2, h2); 37 | let p3 = rolling_pin(&Workplane::zx(), r1, h1, r2, h2); 38 | 39 | let cutout = p1.union(&p2).union(&p3).into_shape(); 40 | 41 | cube(edge_length).subtract(&cutout).into_shape() 42 | } 43 | 44 | fn cube(edge_length: f64) -> Shape { 45 | let wp = Workplane::xy(); 46 | let wp = wp.translated(DVec3::new(0.0, 0.0, -0.5 * edge_length)); 47 | let wire = wp.rect(edge_length, edge_length); 48 | wire.to_face().extrude(DVec3::new(0.0, 0.0, edge_length)).into_shape() 49 | } 50 | 51 | fn rolling_pin(wp: &Workplane, r1: f64, h1: f64, r2: f64, h2: f64) -> Shape { 52 | let c_long = cylinder(wp, r1, h1); 53 | let c_wide = cylinder(wp, r2, h2); 54 | c_long.union(&c_wide).into_shape() 55 | } 56 | 57 | fn cylinder(wp: &Workplane, radius: f64, height: f64) -> Shape { 58 | let wp = wp.translated(DVec3::new(0.0, 0.0, -0.5 * height)); 59 | let wire = wp.circle(0.0, 0.0, radius); 60 | wire.to_face().extrude(height * wp.normal()).into_shape() 61 | } 62 | -------------------------------------------------------------------------------- /examples/src/variable_fillet.rs: -------------------------------------------------------------------------------- 1 | use glam::dvec3; 2 | use opencascade::{ 3 | primitives::{approximate_function, Direction, IntoShape, Shape}, 4 | workplane::Workplane, 5 | }; 6 | 7 | // Demonstrates a variable fillet radius along the edge of a cube. 8 | pub fn shape() -> Shape { 9 | let base = Workplane::xy().rect(50.0, 50.0); 10 | let mut shape = base.to_face().extrude(dvec3(0.0, 0.0, 50.0)).into_shape(); 11 | 12 | let mut right_face_edges = shape.faces().farthest(Direction::PosX).edges(); 13 | let first_edge = right_face_edges.next().unwrap(); 14 | let another_edge = right_face_edges.next().unwrap(); 15 | 16 | // Manually define fillet radii at normalized 't' values (0-1), where 17 | // t is 0 at the start of the edge, and 1 at the end of the edge. 18 | shape = shape.variable_fillet_edge( 19 | [(0.0, 7.0), (0.2, 20.0), (0.5, 3.0), (0.8, 20.0), (1.0, 7.0)], 20 | &first_edge, 21 | ); 22 | 23 | // Or define fillet radii by providing n, the number of radii to generate, 24 | // and a function which accepts t and returns a radius for the fillet at t. 25 | let num_radii = 5; 26 | shape = shape.variable_fillet_edge( 27 | approximate_function(num_radii, |t| { 28 | let t_squared = t * t; 29 | let val = t_squared / (2.0 * (t_squared - t) + 1.0); 30 | (val + 0.2) * 10.0 31 | }), 32 | &another_edge, 33 | ); 34 | 35 | let left_face_edges = shape.faces().farthest(Direction::NegX).edges(); 36 | 37 | // Fillet all edges on the left face with a rough bell curve, for fun. 38 | shape.variable_fillet_edges( 39 | approximate_function(num_radii, |t| { 40 | let val = ((2.0 * std::f64::consts::PI * (t - 1.0 / 4.0)).sin() + 1.0) / 2.0; 41 | val * 10.0 42 | }), 43 | left_face_edges, 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/write_model.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsStr, path::PathBuf}; 2 | 3 | use clap::{Parser, ValueEnum}; 4 | use examples::Example; 5 | 6 | /// Save an example model to a file 7 | #[derive(Debug, Clone, Parser)] 8 | struct Args { 9 | /// Example to save 10 | example: Example, 11 | 12 | /// Output file path 13 | #[clap(short, long, default_value = "output.step")] 14 | output: PathBuf, 15 | 16 | /// Output format 17 | #[clap(short, long)] 18 | format: Option, 19 | } 20 | 21 | #[derive(Debug, Clone, ValueEnum)] 22 | enum Format { 23 | Step, 24 | Stl, 25 | Iges, 26 | } 27 | 28 | fn main() { 29 | let args = Args::parse(); 30 | let model = args.example.shape(); 31 | 32 | let format = args.format.unwrap_or_else(|| { 33 | let extension = args.output.extension().unwrap_or_else(|| { 34 | panic!("Cannot guess format because the output file name has no extension. Use the '-f' or '--format' flag to specify a format.") 35 | }); 36 | 37 | determine_format(extension) 38 | .unwrap_or_else(|| panic!("Cannot guess format from extension {:?}. Use the '-f' or '--format' flag to specify a format.", extension)) 39 | }); 40 | 41 | match format { 42 | Format::Iges => model.write_iges(args.output).unwrap(), 43 | Format::Step => model.write_step(args.output).unwrap(), 44 | Format::Stl => model.write_stl(args.output).unwrap(), 45 | } 46 | } 47 | 48 | fn determine_format(extension: &OsStr) -> Option { 49 | match extension.to_string_lossy().as_bytes() { 50 | b"step" | b"stp" => Some(Format::Step), 51 | b"stl" => Some(Format::Stl), 52 | b"iges" | b"igs" => Some(Format::Iges), 53 | _ => None, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/src/zbox_case.rs: -------------------------------------------------------------------------------- 1 | use glam::dvec3; 2 | use opencascade::{ 3 | primitives::{Direction, Shape}, 4 | workplane::Workplane, 5 | }; 6 | 7 | pub fn shape() -> Shape { 8 | // The origin of the coordinate system is the closest bottom left corner of 9 | // the PC box, when viewing its ports from behind. 10 | let case_thickness = 2.0; 11 | let case_width = 212.0; 12 | let case_height = 60.0; 13 | let case_depth = 64.0; // Not measured, arbitrary value 14 | 15 | let hook_thickness = 3.0; 16 | let wire_gap = 2.6; 17 | 18 | let case_box = Shape::box_with_dimensions(case_width, case_height, case_depth); 19 | 20 | let back_face = case_box.faces().farthest(Direction::PosY); 21 | 22 | let case_box = case_box.hollow(case_thickness, [back_face]); 23 | 24 | let port_cutout = Workplane::xz() 25 | .sketch() 26 | .move_to(10.0, 9.0) 27 | .line_to(10.0, 55.0) 28 | .line_to(200.0, 55.0) 29 | .line_to(200.0, 9.0) 30 | .close() 31 | .to_face(); 32 | 33 | let cutout = port_cutout.extrude(dvec3(0.0, -case_thickness, 0.0)).into(); 34 | 35 | let mut case_box = case_box.subtract(&cutout); 36 | 37 | // Add the back hooks 38 | let bottom_face = case_box.faces().farthest(Direction::NegZ); 39 | 40 | for x_offset in [-75.0, -25.0, 25.0, 75.0] { 41 | let mut hook: Shape = bottom_face 42 | .workplane() 43 | .translated(dvec3(x_offset, -20.0, 0.0)) 44 | .rect(40.0, 20.0) 45 | .to_face() 46 | .extrude(dvec3(0.0, 0.0, -(hook_thickness + wire_gap))) 47 | .into(); 48 | 49 | let hook_bottom = hook.faces().farthest(Direction::NegY); 50 | 51 | let hook_descent = hook_bottom 52 | .workplane() 53 | .translated(dvec3(0.0, -(hook_thickness + wire_gap) / 2.0 + hook_thickness / 2.0, 0.0)) 54 | .rect(40.0, hook_thickness) 55 | .to_face() 56 | .extrude(dvec3(0.0, -20.0, 0.0)) 57 | .into(); 58 | 59 | hook = hook.union(&hook_descent).into(); 60 | 61 | let bottom_hook_edges = 62 | hook.faces().farthest(Direction::NegY).edges().parallel_to(Direction::PosZ); 63 | hook = hook.fillet_edges(10.0, bottom_hook_edges); 64 | 65 | case_box = case_box.union(&hook); 66 | } 67 | 68 | // Punch some holes in the back for optional zipties 69 | for x_offset in [-100.0, -50.0, 0.0, 50.0, 100.0] { 70 | let ziptie_hole = bottom_face 71 | .workplane() 72 | .translated(dvec3(x_offset, -20.0, 0.0)) 73 | .circle(0.0, 0.0, 2.25) 74 | .to_face() 75 | .extrude(dvec3(0.0, 0.0, 10.0)) 76 | .into(); 77 | 78 | case_box = case_box.subtract(&ziptie_hole) 79 | } 80 | 81 | // Cut out a circle on the front 82 | let front_face = case_box 83 | .faces() 84 | .farthest(Direction::PosZ) 85 | .workplane() 86 | .translated(dvec3(0.0, 75.0, 0.0)) 87 | .circle(0.0, 0.0, 100.0) 88 | .to_face(); 89 | 90 | front_face.subtractive_extrude(&case_box, -case_thickness) 91 | } 92 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | indent_style = "Block" 2 | use_small_heuristics="Max" 3 | imports_granularity="Crate" 4 | match_block_trailing_comma = true 5 | reorder_impl_items = true 6 | use_field_init_shorthand = true 7 | use_try_shorthand = true 8 | --------------------------------------------------------------------------------