├── .github └── workflows │ ├── clippy.yml │ └── pre-commit.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── 00_lego.png ├── 00_lego.rs ├── 00_lego.stl ├── 01_hinge.rs └── 01_hinge.stl ├── git-convential-commits.json └── src ├── core ├── angle.rs ├── axis.rs ├── dir.rs ├── edge.rs ├── intof64.rs ├── length.rs ├── mod.rs ├── path.rs ├── plane.rs └── point.rs ├── errors.rs ├── faces ├── face.rs ├── iterator.rs └── mod.rs ├── lib.rs ├── meshes ├── mod.rs └── render_mesh.rs ├── parts ├── mod.rs ├── part.rs └── primitives │ ├── cube.rs │ ├── cuboid.rs │ ├── cylinder.rs │ ├── mod.rs │ └── sphere.rs └── sketches ├── mod.rs ├── primitives ├── circle.rs ├── mod.rs ├── rectangle.rs └── square.rs └── sketch.rs /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: clippy 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | clippy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Cache Cargo registry 17 | uses: actions/cache@v3 18 | with: 19 | path: ~/.cargo/registry 20 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-cargo-registry- 23 | 24 | - name: Cache Cargo index 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/.cargo/git 28 | key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-cargo-git- 31 | 32 | - name: Cache build artifacts 33 | uses: actions/cache@v3 34 | with: 35 | path: target 36 | key: ${{ runner.os }}-cargo-target-${{ hashFiles('**/Cargo.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-cargo-target- 39 | 40 | - name: Build the Rust project 41 | run: cargo clippy --all-targets --all-features -- -D warnings 42 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Cache Cargo registry 17 | uses: actions/cache@v3 18 | with: 19 | path: ~/.cargo/registry 20 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-cargo-registry- 23 | 24 | - name: Cache Cargo index 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/.cargo/git 28 | key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-cargo-git- 31 | 32 | - name: Cache build artifacts 33 | uses: actions/cache@v3 34 | with: 35 | path: target 36 | key: ${{ runner.os }}-cargo-target-${{ hashFiles('**/Cargo.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-cargo-target- 39 | 40 | - name: Build the Rust project 41 | run: cargo build 42 | 43 | - name: Set up Python (for pre-commit) 44 | uses: actions/setup-python@v4 45 | with: 46 | python-version: '3.x' 47 | 48 | - name: Install pre-commit 49 | run: pip install pre-commit 50 | 51 | - name: Run pre-commit on all files 52 | run: pre-commit run --all-files 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .vscode/ 3 | local/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: [commit-msg, pre-commit] 2 | default_stages: [pre-commit, pre-merge-commit] 3 | minimum_pre_commit_version: 3.2.0 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v5.0.0 7 | hooks: 8 | - id: check-added-large-files 9 | - id: check-ast 10 | - id: check-builtin-literals 11 | - id: check-case-conflict 12 | - id: check-executables-have-shebangs 13 | - id: check-json 14 | - id: check-merge-conflict 15 | - id: check-symlinks 16 | - id: check-toml 17 | - id: check-vcs-permalinks 18 | - id: check-xml 19 | - id: check-yaml 20 | - id: debug-statements 21 | - id: destroyed-symlinks 22 | 23 | - repo: https://github.com/doublify/pre-commit-rust 24 | rev: v1.0 25 | hooks: 26 | - id: fmt 27 | name: cargo fmt 28 | 29 | - repo: local 30 | hooks: 31 | - id: cargo-test 32 | name: cargo test 33 | description: Run tests 34 | entry: cargo test 35 | language: system 36 | pass_filenames: false 37 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.10" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 10 | 11 | [[package]] 12 | name = "anvil" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "assert_float_eq", 16 | "cxx", 17 | "iter_fixed", 18 | "opencascade-sys", 19 | "tempdir", 20 | "tempfile", 21 | ] 22 | 23 | [[package]] 24 | name = "assert_float_eq" 25 | version = "1.1.4" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "10d2119f741b79fe9907f5396d19bffcb46568cfcc315e78677d731972ac7085" 28 | 29 | [[package]] 30 | name = "bitflags" 31 | version = "2.9.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 34 | 35 | [[package]] 36 | name = "cc" 37 | version = "1.2.21" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" 40 | dependencies = [ 41 | "shlex", 42 | ] 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "clap" 52 | version = "4.5.37" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 55 | dependencies = [ 56 | "clap_builder", 57 | ] 58 | 59 | [[package]] 60 | name = "clap_builder" 61 | version = "4.5.37" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 64 | dependencies = [ 65 | "anstyle", 66 | "clap_lex", 67 | "strsim", 68 | ] 69 | 70 | [[package]] 71 | name = "clap_lex" 72 | version = "0.7.4" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 75 | 76 | [[package]] 77 | name = "cmake" 78 | version = "0.1.54" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 81 | dependencies = [ 82 | "cc", 83 | ] 84 | 85 | [[package]] 86 | name = "codespan-reporting" 87 | version = "0.12.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" 90 | dependencies = [ 91 | "serde", 92 | "termcolor", 93 | "unicode-width", 94 | ] 95 | 96 | [[package]] 97 | name = "cxx" 98 | version = "1.0.158" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "a71ea7f29c73f7ffa64c50b83c9fe4d3a6d4be89a86b009eb80d5a6d3429d741" 101 | dependencies = [ 102 | "cc", 103 | "cxxbridge-cmd", 104 | "cxxbridge-flags", 105 | "cxxbridge-macro", 106 | "foldhash", 107 | "link-cplusplus", 108 | ] 109 | 110 | [[package]] 111 | name = "cxx-build" 112 | version = "1.0.158" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "36a8232661d66dcf713394726157d3cfe0a89bfc85f52d6e9f9bbc2306797fe7" 115 | dependencies = [ 116 | "cc", 117 | "codespan-reporting", 118 | "proc-macro2", 119 | "quote", 120 | "scratch", 121 | "syn", 122 | ] 123 | 124 | [[package]] 125 | name = "cxxbridge-cmd" 126 | version = "1.0.158" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "4f44296c8693e9ea226a48f6a122727f77aa9e9e338380cb021accaeeb7ee279" 129 | dependencies = [ 130 | "clap", 131 | "codespan-reporting", 132 | "proc-macro2", 133 | "quote", 134 | "syn", 135 | ] 136 | 137 | [[package]] 138 | name = "cxxbridge-flags" 139 | version = "1.0.158" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "c42f69c181c176981ae44ba9876e2ea41ce8e574c296b38d06925ce9214fb8e4" 142 | 143 | [[package]] 144 | name = "cxxbridge-macro" 145 | version = "1.0.158" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "8faff5d4467e0709448187df29ccbf3b0982cc426ee444a193f87b11afb565a8" 148 | dependencies = [ 149 | "proc-macro2", 150 | "quote", 151 | "rustversion", 152 | "syn", 153 | ] 154 | 155 | [[package]] 156 | name = "errno" 157 | version = "0.3.11" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 160 | dependencies = [ 161 | "libc", 162 | "windows-sys", 163 | ] 164 | 165 | [[package]] 166 | name = "fastrand" 167 | version = "2.3.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 170 | 171 | [[package]] 172 | name = "foldhash" 173 | version = "0.1.5" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 176 | 177 | [[package]] 178 | name = "fuchsia-cprng" 179 | version = "0.1.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 182 | 183 | [[package]] 184 | name = "getrandom" 185 | version = "0.3.2" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 188 | dependencies = [ 189 | "cfg-if", 190 | "libc", 191 | "r-efi", 192 | "wasi", 193 | ] 194 | 195 | [[package]] 196 | name = "iter_fixed" 197 | version = "0.4.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "592ff74cdc6a923b2ae357dad7db2f5dcbf5d4ace9afcf3ab7c7f20b209fd949" 200 | 201 | [[package]] 202 | name = "libc" 203 | version = "0.2.172" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 206 | 207 | [[package]] 208 | name = "link-cplusplus" 209 | version = "1.0.10" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" 212 | dependencies = [ 213 | "cc", 214 | ] 215 | 216 | [[package]] 217 | name = "linux-raw-sys" 218 | version = "0.9.4" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 221 | 222 | [[package]] 223 | name = "occt-sys" 224 | version = "0.6.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "a1b3fb218c80b037a612cc4ec325a426a1705d0b2e25b207f7e3db22556ded5f" 227 | dependencies = [ 228 | "cmake", 229 | ] 230 | 231 | [[package]] 232 | name = "once_cell" 233 | version = "1.21.3" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 236 | 237 | [[package]] 238 | name = "opencascade-sys" 239 | version = "0.2.0" 240 | source = "git+https://github.com/bschwind/opencascade-rs?rev=c30da56647c2a60393984458439180886ecaf951#c30da56647c2a60393984458439180886ecaf951" 241 | dependencies = [ 242 | "cmake", 243 | "cxx", 244 | "cxx-build", 245 | "occt-sys", 246 | ] 247 | 248 | [[package]] 249 | name = "proc-macro2" 250 | version = "1.0.95" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 253 | dependencies = [ 254 | "unicode-ident", 255 | ] 256 | 257 | [[package]] 258 | name = "quote" 259 | version = "1.0.40" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 262 | dependencies = [ 263 | "proc-macro2", 264 | ] 265 | 266 | [[package]] 267 | name = "r-efi" 268 | version = "5.2.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 271 | 272 | [[package]] 273 | name = "rand" 274 | version = "0.4.6" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 277 | dependencies = [ 278 | "fuchsia-cprng", 279 | "libc", 280 | "rand_core 0.3.1", 281 | "rdrand", 282 | "winapi", 283 | ] 284 | 285 | [[package]] 286 | name = "rand_core" 287 | version = "0.3.1" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 290 | dependencies = [ 291 | "rand_core 0.4.2", 292 | ] 293 | 294 | [[package]] 295 | name = "rand_core" 296 | version = "0.4.2" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 299 | 300 | [[package]] 301 | name = "rdrand" 302 | version = "0.4.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 305 | dependencies = [ 306 | "rand_core 0.3.1", 307 | ] 308 | 309 | [[package]] 310 | name = "remove_dir_all" 311 | version = "0.5.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 314 | dependencies = [ 315 | "winapi", 316 | ] 317 | 318 | [[package]] 319 | name = "rustix" 320 | version = "1.0.7" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 323 | dependencies = [ 324 | "bitflags", 325 | "errno", 326 | "libc", 327 | "linux-raw-sys", 328 | "windows-sys", 329 | ] 330 | 331 | [[package]] 332 | name = "rustversion" 333 | version = "1.0.20" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 336 | 337 | [[package]] 338 | name = "scratch" 339 | version = "1.0.8" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52" 342 | 343 | [[package]] 344 | name = "serde" 345 | version = "1.0.219" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 348 | dependencies = [ 349 | "serde_derive", 350 | ] 351 | 352 | [[package]] 353 | name = "serde_derive" 354 | version = "1.0.219" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 357 | dependencies = [ 358 | "proc-macro2", 359 | "quote", 360 | "syn", 361 | ] 362 | 363 | [[package]] 364 | name = "shlex" 365 | version = "1.3.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 368 | 369 | [[package]] 370 | name = "strsim" 371 | version = "0.11.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 374 | 375 | [[package]] 376 | name = "syn" 377 | version = "2.0.101" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 380 | dependencies = [ 381 | "proc-macro2", 382 | "quote", 383 | "unicode-ident", 384 | ] 385 | 386 | [[package]] 387 | name = "tempdir" 388 | version = "0.3.7" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 391 | dependencies = [ 392 | "rand", 393 | "remove_dir_all", 394 | ] 395 | 396 | [[package]] 397 | name = "tempfile" 398 | version = "3.19.1" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 401 | dependencies = [ 402 | "fastrand", 403 | "getrandom", 404 | "once_cell", 405 | "rustix", 406 | "windows-sys", 407 | ] 408 | 409 | [[package]] 410 | name = "termcolor" 411 | version = "1.4.1" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 414 | dependencies = [ 415 | "winapi-util", 416 | ] 417 | 418 | [[package]] 419 | name = "unicode-ident" 420 | version = "1.0.18" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 423 | 424 | [[package]] 425 | name = "unicode-width" 426 | version = "0.2.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 429 | 430 | [[package]] 431 | name = "wasi" 432 | version = "0.14.2+wasi-0.2.4" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 435 | dependencies = [ 436 | "wit-bindgen-rt", 437 | ] 438 | 439 | [[package]] 440 | name = "winapi" 441 | version = "0.3.9" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 444 | dependencies = [ 445 | "winapi-i686-pc-windows-gnu", 446 | "winapi-x86_64-pc-windows-gnu", 447 | ] 448 | 449 | [[package]] 450 | name = "winapi-i686-pc-windows-gnu" 451 | version = "0.4.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 454 | 455 | [[package]] 456 | name = "winapi-util" 457 | version = "0.1.9" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 460 | dependencies = [ 461 | "windows-sys", 462 | ] 463 | 464 | [[package]] 465 | name = "winapi-x86_64-pc-windows-gnu" 466 | version = "0.4.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 469 | 470 | [[package]] 471 | name = "windows-sys" 472 | version = "0.59.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 475 | dependencies = [ 476 | "windows-targets", 477 | ] 478 | 479 | [[package]] 480 | name = "windows-targets" 481 | version = "0.52.6" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 484 | dependencies = [ 485 | "windows_aarch64_gnullvm", 486 | "windows_aarch64_msvc", 487 | "windows_i686_gnu", 488 | "windows_i686_gnullvm", 489 | "windows_i686_msvc", 490 | "windows_x86_64_gnu", 491 | "windows_x86_64_gnullvm", 492 | "windows_x86_64_msvc", 493 | ] 494 | 495 | [[package]] 496 | name = "windows_aarch64_gnullvm" 497 | version = "0.52.6" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 500 | 501 | [[package]] 502 | name = "windows_aarch64_msvc" 503 | version = "0.52.6" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 506 | 507 | [[package]] 508 | name = "windows_i686_gnu" 509 | version = "0.52.6" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 512 | 513 | [[package]] 514 | name = "windows_i686_gnullvm" 515 | version = "0.52.6" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 518 | 519 | [[package]] 520 | name = "windows_i686_msvc" 521 | version = "0.52.6" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 524 | 525 | [[package]] 526 | name = "windows_x86_64_gnu" 527 | version = "0.52.6" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 530 | 531 | [[package]] 532 | name = "windows_x86_64_gnullvm" 533 | version = "0.52.6" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 536 | 537 | [[package]] 538 | name = "windows_x86_64_msvc" 539 | version = "0.52.6" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 542 | 543 | [[package]] 544 | name = "wit-bindgen-rt" 545 | version = "0.39.0" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 548 | dependencies = [ 549 | "bitflags", 550 | ] 551 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anvil" 3 | version = "0.1.0" 4 | edition = "2024" 5 | description = "An intuitive 3D modeling kernel" 6 | repository = "https://github.com/paramatrix-dev/anvil" 7 | readme = "README.md" 8 | license = "LGPL-2.1-only" 9 | keywords = [ 10 | "3d", 11 | "3d-design", 12 | "3d-modeling", 13 | "cad", 14 | "cae", 15 | "computer aided design", 16 | "computer aided engineering", 17 | "design", 18 | "modeling", 19 | "occt", 20 | "opencascade", 21 | ] 22 | 23 | include = [ 24 | "/src", 25 | "Cargo.toml", 26 | "LICENSE", 27 | "README.md", 28 | "/examples/00_lego.png", 29 | ] 30 | 31 | [features] 32 | default = ["builtin"] 33 | builtin = [ "opencascade-sys/builtin" ] 34 | 35 | [dependencies] 36 | cxx = "1" 37 | iter_fixed = "0.4.0" 38 | opencascade-sys = { git = "https://github.com/bschwind/opencascade-rs", rev = "c30da56647c2a60393984458439180886ecaf951" } 39 | tempfile = "3.19.1" 40 | 41 | [dev-dependencies] 42 | tempdir = "0.3.7" 43 | assert_float_eq = "1.1.4" 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is Anvil 2 | 3 | Anvil is an intuitive, easy-to-use Rust crate for building 3D CAD models. It is built on the principles of: 4 | - **consistent and predictable APIs**: APIs are consistent between 2D and 3D modelling and between different shapes 5 | - **mandatory unit support**: Anvil forces you to specify units for all lengths and angles, giving you certainty about the true dimensions of your models 6 | - **reliability by design**: unit and integration tests exist for almost all public APIs communicating functionality and ensuring reliability 7 | 8 | # Installation 9 | 10 | Anvil is not yet on crates.io, so to install it you need to run 11 | ```bash 12 | cargo add --git "https://github.com/paramatrix-dev/anvil.git" 13 | ``` 14 | or add 15 | ```toml 16 | anvil = { git = "https://github.com/paramatrix-dev/anvil.git", branch = "main" } 17 | ``` 18 | to your Cargo.toml `[dependencies]` section. 19 | 20 | # Usage 21 | 22 | The two main structs in Anvil are `anvil::Part` for 3D and `anvil::Sketch` for 2D models. Both have primitive constructor-structs like `anvil::Cuboid` for `Part` or `anvil::Rectangle` for `Sketch` which can be further designed with operations like `add`, `subtract`, and `interface`. This is how you would create a 2x2 Lego brick in Anvil: 23 | ```rust 24 | use anvil::{Axis, Cuboid, Cylinder, IntoLength, Part, point}; 25 | 26 | let block_width = 16.mm(); 27 | let block_height = 9.6.mm(); 28 | let stud_height = 11.2.mm() - block_height; 29 | let stud_distance = 8.mm(); 30 | let stud_diameter = 4.8.mm(); 31 | let thickness = 1.2.mm(); 32 | let tube_diameter = 6.5.mm(); 33 | 34 | let hollow_block_width = block_width - thickness; 35 | 36 | let block = Cuboid::from_dim(block_width, block_width, block_height); 37 | let studs = Cylinder::from_diameter(stud_diameter, stud_height) 38 | .move_to(point!( 39 | stud_distance / 2., 40 | stud_distance / 2., 41 | (block_height + stud_height) / 2. 42 | )) 43 | .circular_pattern(Axis::<3>::z(), 4); 44 | let inner_block = Cuboid::from_dim(hollow_block_width, hollow_block_width, block_height) 45 | .move_to(point!(0.m(), 0.m(), thickness * -0.5)); 46 | let inner_tube = Cylinder::from_diameter(tube_diameter, block_height - thickness).subtract( 47 | &Cylinder::from_diameter(tube_diameter - thickness / 2., block_height - thickness), 48 | ); 49 | 50 | let part = block.add(&studs).subtract(&inner_block).add(&inner_tube); 51 | ``` 52 | ![](/examples/00_lego.png) 53 | 54 | For more examples, have a look at the `/examples` directory. 55 | 56 | # Inspiration 57 | 58 | Anvil was originally inspired by the [opencascade](https://crates.io/crates/opencascade) crate. `opencascade-rs` was first used as the basis for another project, but we quickly realized a few drawbacks of the crate: 59 | - basic functionality like `Clone` and `PartialEq` is missing 60 | - hardly any function, struct, or enum are documented or tested 61 | - the APIs are inconsistent and feel unintuitive 62 | 63 | However, the OpenCascade bindings of the adjacent [opencascade-sys](https://crates.io/crates/opencascade-sys) crate were very useful and used as the backend of Anvil. In the future, we might try to eliminate OpenCascade entirely and implement a custom kernel in Rust. 64 | -------------------------------------------------------------------------------- /examples/00_lego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paramatrix-dev/anvil/6df82b9265ae3c76b2a15ebcf314b22de6966add/examples/00_lego.png -------------------------------------------------------------------------------- /examples/00_lego.rs: -------------------------------------------------------------------------------- 1 | use anvil::{Axis, Cuboid, Cylinder, IntoLength, Part, point}; 2 | 3 | fn construct() -> Part { 4 | let block_width = 16.mm(); 5 | let block_height = 9.6.mm(); 6 | let stud_height = 11.2.mm() - block_height; 7 | let stud_distance = 8.mm(); 8 | let stud_diameter = 4.8.mm(); 9 | let thickness = 1.2.mm(); 10 | let tube_diameter = 6.5.mm(); 11 | 12 | let hollow_block_width = block_width - thickness; 13 | 14 | let block = Cuboid::from_dim(block_width, block_width, block_height); 15 | let studs = Cylinder::from_diameter(stud_diameter, stud_height) 16 | .move_to(point!( 17 | stud_distance / 2., 18 | stud_distance / 2., 19 | (block_height + stud_height) / 2. 20 | )) 21 | .circular_pattern(Axis::<3>::z(), 4); 22 | let inner_block = Cuboid::from_dim(hollow_block_width, hollow_block_width, block_height) 23 | .move_to(point!(0.m(), 0.m(), thickness * -0.5)); 24 | let inner_tube = Cylinder::from_diameter(tube_diameter, block_height - thickness).subtract( 25 | &Cylinder::from_diameter(tube_diameter - thickness / 2., block_height - thickness), 26 | ); 27 | 28 | block.add(&studs).subtract(&inner_block).add(&inner_tube) 29 | } 30 | 31 | fn main() { 32 | let part = construct(); 33 | part.write_stl("examples/00_lego.stl") 34 | .expect("could not write part to .STL"); 35 | } 36 | -------------------------------------------------------------------------------- /examples/01_hinge.rs: -------------------------------------------------------------------------------- 1 | use anvil::{Axis, Cylinder, IntoAngle, IntoLength, Part, Path, Plane, point}; 2 | 3 | fn construct() -> Part { 4 | let thickness = 1.m(); 5 | let depth = 7.m(); 6 | let mount_distance = 7.m(); 7 | let mount_radius = 0.5.m(); 8 | let hinge_radius = 2.5.m(); 9 | 10 | let hinge_outer_radius = hinge_radius + thickness; 11 | let total_height = hinge_outer_radius + 2. * thickness; 12 | 13 | let part = Path::at(point!(0, 0)) 14 | .line_by(0.m(), thickness) 15 | .line_by(mount_distance - hinge_outer_radius, 0.m()) 16 | .arc_by(thickness, 90.deg()) 17 | .line_by(0.m(), hinge_outer_radius - 2. * thickness) 18 | .arc_by(-hinge_outer_radius, 180.deg()) 19 | .line_by(0.m(), -hinge_outer_radius + 2. * thickness) 20 | .arc_by(thickness, 90.deg()) 21 | .line_by(mount_distance - hinge_outer_radius, 0.m()) 22 | .line_by(0.m(), -thickness) 23 | .close() 24 | .extrude(Plane::yz(), depth) 25 | .unwrap() 26 | .move_to(point!(thickness / 4., 0.m(), total_height / 2.)); 27 | 28 | let hinge_hole = Cylinder::from_radius(hinge_radius, depth + thickness) 29 | .rotate_around(Axis::<3>::y(), 90.deg()) 30 | .move_to(point!(0.m(), 0.m(), hinge_outer_radius)); 31 | 32 | let mount_hole = Cylinder::from_radius(mount_radius, thickness * 2.); 33 | 34 | part.subtract(&hinge_hole) 35 | .subtract(&mount_hole.move_by(depth / 2. - thickness, mount_distance, thickness / 2.)) 36 | .subtract(&mount_hole.move_by(depth / 2. - thickness, -mount_distance, thickness / 2.)) 37 | .subtract(&mount_hole.move_by(-depth / 2. + thickness, mount_distance, thickness / 2.)) 38 | .subtract(&mount_hole.move_by(-depth / 2. + thickness, -mount_distance, thickness / 2.)) 39 | } 40 | 41 | fn main() { 42 | let part = construct(); 43 | part.write_stl_with_tolerance("examples/01_hinge.stl", 0.01) 44 | .expect("could not write part to .STL"); 45 | } 46 | -------------------------------------------------------------------------------- /git-convential-commits.json: -------------------------------------------------------------------------------- 1 | { 2 | "convention" : { 3 | "commitTypes": [ 4 | "build", 5 | "chore", 6 | "ci", 7 | "docs", 8 | "feat", 9 | "fix", 10 | "merge", 11 | "perf", 12 | "refactor", 13 | "revert", 14 | "test", 15 | 16 | "lint", 17 | "style" 18 | ], 19 | "commitScopes": [] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/core/angle.rs: -------------------------------------------------------------------------------- 1 | use core::f64; 2 | use std::ops::{Add, Div, Mul, Neg, Sub}; 3 | 4 | use super::IntoF64; 5 | 6 | /// A physical angle (i.e. a distance). 7 | /// 8 | /// Angle exists to remove ambiguity about angle units, which are not supported by default by 9 | /// major CAD kernels. 10 | /// 11 | /// ```rust 12 | /// use anvil::Angle; 13 | /// 14 | /// // You can construct an angle using the Angle::from_[unit] methods: 15 | /// let degrees_angle = Angle::from_deg(1.2); 16 | /// let radians_angle = Angle::from_rad(3.4); 17 | /// 18 | /// // To get back a angle value in a specific unit, call the Angle.[unit] method 19 | /// assert_eq!(degrees_angle.deg(), 1.2); 20 | /// assert_eq!(radians_angle.rad(), 3.4); 21 | /// 22 | /// // Angle construction can be simplified using the `IntoAngle` trait. 23 | /// use anvil::IntoAngle; 24 | /// 25 | /// assert_eq!(1.2.deg(), Angle::from_deg(1.2)); 26 | /// assert_eq!(4.5.rad(), Angle::from_rad(4.5)); 27 | /// ``` 28 | #[derive(Debug, PartialEq, Copy, Clone, PartialOrd)] 29 | pub struct Angle { 30 | rad: f64, 31 | } 32 | impl Angle { 33 | /// Construct a `Angle` with a value of zero. 34 | /// 35 | /// # Example 36 | /// ```rust 37 | /// use anvil::Angle; 38 | /// 39 | /// let angle = Angle::zero(); 40 | /// assert_eq!(angle.deg(), 0.); 41 | /// ``` 42 | pub fn zero() -> Self { 43 | Self { rad: 0. } 44 | } 45 | /// Construct a `Angle` from a value in radians. 46 | /// 47 | /// # Example 48 | /// ```rust 49 | /// use core::f64; 50 | /// use anvil::Angle; 51 | /// 52 | /// let angle = Angle::from_rad(f64::consts::PI); 53 | /// assert_eq!(angle.deg(), 180.); 54 | /// ``` 55 | pub fn from_rad(value: f64) -> Self { 56 | Self { 57 | rad: value % f64::consts::TAU, 58 | } 59 | } 60 | /// Return the value of this angle in radians. 61 | pub fn rad(&self) -> f64 { 62 | self.rad 63 | } 64 | /// Construct a `Angle` from a value in degrees. 65 | /// 66 | /// # Example 67 | /// ```rust 68 | /// use core::f64; 69 | /// use anvil::Angle; 70 | /// 71 | /// let angle = Angle::from_deg(180.); 72 | /// assert_eq!(angle.rad(), f64::consts::PI); 73 | /// ``` 74 | pub fn from_deg(value: f64) -> Self { 75 | Angle { 76 | rad: value / 360. * f64::consts::TAU, 77 | } 78 | } 79 | /// Return the value of this angle in degrees. 80 | pub fn deg(&self) -> f64 { 81 | self.rad / f64::consts::TAU * 360. 82 | } 83 | 84 | /// Return the absolute value of this `Angle`. 85 | /// 86 | /// ```rust 87 | /// use anvil::IntoAngle; 88 | /// 89 | /// assert_eq!((-45).deg().abs(), 45.deg()); 90 | /// assert_eq!(10.deg().abs(), 10.deg()); 91 | /// ``` 92 | pub fn abs(&self) -> Self { 93 | Self { 94 | rad: self.rad.abs(), 95 | } 96 | } 97 | 98 | /// Return the smaller of two angles. 99 | /// 100 | /// # Example 101 | /// ```rust 102 | /// use anvil::IntoAngle; 103 | /// 104 | /// let angle1 = 1.deg(); 105 | /// let angle2 = 2.deg(); 106 | /// assert_eq!(angle1.min(&angle2), angle1); 107 | /// assert_eq!(angle2.min(&angle1), angle1); 108 | /// ``` 109 | pub fn min(&self, other: &Self) -> Self { 110 | Angle { 111 | rad: self.rad.min(other.rad), 112 | } 113 | } 114 | /// Return the larger of two lengths. 115 | /// 116 | /// # Example 117 | /// ```rust 118 | /// use anvil::IntoAngle; 119 | /// 120 | /// let angle1 = 1.deg(); 121 | /// let angle2 = 2.deg(); 122 | /// assert_eq!(angle1.max(&angle2), angle2); 123 | /// assert_eq!(angle2.max(&angle1), angle2); 124 | /// ``` 125 | pub fn max(&self, other: &Self) -> Self { 126 | Angle { 127 | rad: self.rad.max(other.rad), 128 | } 129 | } 130 | } 131 | 132 | impl Add for Angle { 133 | type Output = Angle; 134 | fn add(self, other: Angle) -> Angle { 135 | Angle { 136 | rad: self.rad + other.rad, 137 | } 138 | } 139 | } 140 | 141 | impl Sub for Angle { 142 | type Output = Angle; 143 | fn sub(self, other: Angle) -> Angle { 144 | Angle { 145 | rad: self.rad - other.rad, 146 | } 147 | } 148 | } 149 | 150 | impl Mul for Angle { 151 | type Output = Angle; 152 | fn mul(self, other: f64) -> Angle { 153 | Angle { 154 | rad: self.rad * other, 155 | } 156 | } 157 | } 158 | 159 | impl Mul for f64 { 160 | type Output = Angle; 161 | fn mul(self, other: Angle) -> Angle { 162 | other * self 163 | } 164 | } 165 | 166 | impl Div for Angle { 167 | type Output = Angle; 168 | fn div(self, other: f64) -> Angle { 169 | Angle { 170 | rad: self.rad / other, 171 | } 172 | } 173 | } 174 | 175 | impl Div for Angle { 176 | type Output = f64; 177 | /// Divide a `Angle` by another `Angle`. 178 | /// ```rust 179 | /// use anvil::IntoAngle; 180 | /// 181 | /// assert_eq!(6.deg() / 2.deg(), 3.) 182 | /// ``` 183 | fn div(self, other: Angle) -> f64 { 184 | self.rad / other.rad 185 | } 186 | } 187 | 188 | impl Neg for Angle { 189 | type Output = Angle; 190 | fn neg(self) -> Self::Output { 191 | self * -1. 192 | } 193 | } 194 | 195 | /// Import this trait to easily convert numbers into `Angle`s. 196 | /// 197 | /// ```rust 198 | /// use anvil::{Angle, IntoAngle}; 199 | /// 200 | /// assert_eq!(5.deg(), Angle::from_deg(5.)); 201 | /// assert_eq!(5.123.rad(), Angle::from_rad(5.123)); 202 | /// ``` 203 | pub trait IntoAngle: IntoF64 { 204 | /// Convert this number into a `Angle` in degrees. 205 | /// 206 | /// ```rust 207 | /// use anvil::{IntoAngle, Angle}; 208 | /// 209 | /// assert_eq!(5.deg(), Angle::from_deg(5.)); 210 | /// ``` 211 | fn deg(&self) -> Angle { 212 | Angle::from_deg(self.to_f64()) 213 | } 214 | /// Convert this number into a `Angle` in radians. 215 | /// 216 | /// ```rust 217 | /// use anvil::{IntoAngle, Angle}; 218 | /// 219 | /// assert_eq!(5.rad(), Angle::from_rad(5.)); 220 | /// ``` 221 | fn rad(&self) -> Angle { 222 | Angle::from_rad(self.to_f64()) 223 | } 224 | } 225 | 226 | impl IntoAngle for usize {} 227 | impl IntoAngle for isize {} 228 | impl IntoAngle for u8 {} 229 | impl IntoAngle for u16 {} 230 | impl IntoAngle for u32 {} 231 | impl IntoAngle for u64 {} 232 | impl IntoAngle for u128 {} 233 | impl IntoAngle for i8 {} 234 | impl IntoAngle for i16 {} 235 | impl IntoAngle for i32 {} 236 | impl IntoAngle for i64 {} 237 | impl IntoAngle for i128 {} 238 | impl IntoAngle for f32 {} 239 | impl IntoAngle for f64 {} 240 | 241 | #[cfg(test)] 242 | mod tests { 243 | use super::*; 244 | 245 | #[test] 246 | fn add() { 247 | assert_eq!(2.rad() + 3.rad(), 5.rad()); 248 | } 249 | 250 | #[test] 251 | fn subtract() { 252 | assert_eq!(3.rad() - 2.rad(), 1.rad()); 253 | } 254 | 255 | #[test] 256 | fn multiply_with_f64() { 257 | assert_eq!(0.2.rad() * 4., 0.8.rad()); 258 | assert_eq!(4. * 0.2.rad(), 0.8.rad()); 259 | } 260 | 261 | #[test] 262 | fn divide_with_f64() { 263 | assert_eq!(6.rad() / 2., 3.rad()); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/core/axis.rs: -------------------------------------------------------------------------------- 1 | use cxx::UniquePtr; 2 | use opencascade_sys::ffi; 3 | 4 | use crate::{Dir, Error, Length, Point, dir, point}; 5 | 6 | /// An axis in space. 7 | /// 8 | /// Axes can be two- or three-dimensional. 9 | /// ```rust 10 | /// use anvil::{Axis, IntoLength, dir, point}; 11 | /// 12 | /// let two_dimensional_axis = Axis::<2>::new(point!(1.m(), 2.m()), dir!(3, 4)); 13 | /// let three_dimensional_axis = Axis::<3>::new(point!(1.m(), 2.m(), 3.m()), dir!(4, 5, 6)); 14 | /// ``` 15 | /// 16 | /// Axes can also be constructed from tuples containing a `Point` and a `Dir`, simplifying dimensionality. 17 | /// ```rust 18 | /// use anvil::{Axis, IntoLength, dir, point}; 19 | /// 20 | /// assert_eq!( 21 | /// Axis::<2>::new(point!(1.m(), 2.m()), dir!(3, 4)), 22 | /// (point!(1.m(), 2.m()), dir!(3, 4)).into(), 23 | /// ); 24 | /// assert_eq!( 25 | /// Axis::<3>::new(point!(1.m(), 2.m(), 3.m()), dir!(4, 5, 6)), 26 | /// (point!(1.m(), 2.m(), 3.m()), dir!(4, 5, 6)).into(), 27 | /// ); 28 | /// ``` 29 | #[derive(Debug, PartialEq, Copy, Clone, PartialOrd)] 30 | pub struct Axis { 31 | /// A `Point` contained in the `Axis`. 32 | pub origin: Point, 33 | /// The `Dir` this `Axis` points to. 34 | pub direction: Dir, 35 | } 36 | impl Axis { 37 | /// Construct an `Axis`. 38 | pub fn new(origin: Point, direction: Dir) -> Self { 39 | Self { origin, direction } 40 | } 41 | 42 | /// Construct an `Axis` that lies between two `Point`s. 43 | /// 44 | /// The first point is taken as the axis origin. 45 | /// 46 | /// ```rust 47 | /// use anvil::{Axis, Error, IntoLength, dir, point}; 48 | /// 49 | /// // for 2d 50 | /// assert_eq!( 51 | /// Axis::<2>::between(point!(1.m(), 1.m()), point!(2.m(), 1.m())), 52 | /// Ok(Axis::<2>::new(point!(1.m(), 1.m()), dir!(1, 0))) 53 | /// ); 54 | /// assert_eq!( 55 | /// Axis::<2>::between(point!(1.m(), 1.m()), point!(1.m(), 1.m())), 56 | /// Err(Error::ZeroVector) 57 | /// ); 58 | /// 59 | /// // for 2d 60 | /// assert_eq!( 61 | /// Axis::<3>::between(point!(1.m(), 1.m(), 1.m()), point!(2.m(), 1.m(), 1.m())), 62 | /// Ok(Axis::<3>::new(point!(1.m(), 1.m(), 1.m()), dir!(1, 0, 0))) 63 | /// ); 64 | /// assert_eq!( 65 | /// Axis::<3>::between(point!(1.m(), 1.m(), 1.m()), point!(1.m(), 1.m(), 1.m())), 66 | /// Err(Error::ZeroVector) 67 | /// ); 68 | /// ``` 69 | pub fn between(origin: Point, other: Point) -> Result { 70 | let direction = other.direction_from(origin)?; 71 | Ok(Self { origin, direction }) 72 | } 73 | 74 | /// Return a `Point` on the `Axis` at a specified distance its origin. 75 | /// 76 | /// ```rust 77 | /// use anvil::{Axis, IntoLength, dir, point}; 78 | /// 79 | /// // for 2d 80 | /// let axis = Axis::<2>::new(point!(1.m(), 2.m()), dir!(1, 0)); 81 | /// assert_eq!( 82 | /// axis.point_at(5.m()), 83 | /// point!(6.m(), 2.m()), 84 | /// ); 85 | /// 86 | /// // for 3d 87 | /// let axis = Axis::<3>::new(point!(1.m(), 2.m(), 3.m()), dir!(1, 0, 0)); 88 | /// assert_eq!( 89 | /// axis.point_at(5.m()), 90 | /// point!(6.m(), 2.m(), 3.m()), 91 | /// ); 92 | /// ``` 93 | pub fn point_at(&self, distance: Length) -> Point { 94 | self.origin + self.direction * distance 95 | } 96 | } 97 | impl From<(Point, Dir)> for Axis { 98 | fn from((origin, direction): (Point, Dir)) -> Self { 99 | Axis::new(origin, direction) 100 | } 101 | } 102 | impl From<(Dir, Point)> for Axis { 103 | fn from((direction, origin): (Dir, Point)) -> Self { 104 | Axis::new(origin, direction) 105 | } 106 | } 107 | 108 | impl Axis<2> { 109 | /// Return the `Axis<2>` identical to the x-axis at the origin. 110 | pub fn x() -> Self { 111 | Self::new(point!(0, 0), dir!(1, 0)) 112 | } 113 | /// Return the `Axis<2>` identical to the y-axis at the origin. 114 | pub fn y() -> Self { 115 | Self::new(point!(0, 0), dir!(0, 1)) 116 | } 117 | /// Return the `Axis<2>` identical to the x-axis at the origin in reverse direction. 118 | pub fn neg_x() -> Self { 119 | Self::new(point!(0, 0), dir!(-1, 0)) 120 | } 121 | /// Return the `Axis<2>` identical to the y-axis at the origin in reverse direction. 122 | pub fn neg_y() -> Self { 123 | Self::new(point!(0, 0), dir!(0, -1)) 124 | } 125 | 126 | /// Return the intersection `Point` of this `Axis<2>` with another. 127 | /// 128 | /// If the two axes are parallel, None is returned. 129 | /// 130 | /// ```rust 131 | /// use anvil::{Axis, IntoLength, dir, point}; 132 | /// 133 | /// let axis1 = Axis::<2>::new(point!(0, 0), dir!(1, 1)); 134 | /// let axis2 = Axis::<2>::new(point!(1.m(), 5.m()), dir!(0, 1)); 135 | /// assert_eq!(axis1.intersect(axis2), Some(point!(1.m(), 1.m()))); 136 | /// assert_eq!(axis1.intersect(axis1), None); 137 | /// ``` 138 | pub fn intersect(&self, other: Self) -> Option> { 139 | // TODO: generalize for all dimensions 140 | let determinant = 141 | self.direction.x() * other.direction.y() - self.direction.y() * other.direction.x(); 142 | 143 | let lines_are_parallel = determinant.abs() < 1e-9; 144 | if lines_are_parallel { 145 | return None; 146 | } 147 | 148 | let diff = other.origin - self.origin; 149 | let offset = 150 | (diff.x() * other.direction.y() - diff.y() * other.direction.x()) / determinant; 151 | 152 | Some(self.origin + offset * self.direction) 153 | } 154 | } 155 | 156 | impl Axis<3> { 157 | /// Return the `Axis<3>` identical to the x-axis at the origin. 158 | pub fn x() -> Self { 159 | Self::new(point!(0, 0, 0), dir!(1, 0, 0)) 160 | } 161 | /// Return the `Axis<3>` identical to the y-axis at the origin. 162 | pub fn y() -> Self { 163 | Self::new(point!(0, 0, 0), dir!(0, 1, 0)) 164 | } 165 | /// Return the `Axis<3>` identical to the z-axis at the origin. 166 | pub fn z() -> Self { 167 | Self::new(point!(0, 0, 0), dir!(0, 0, 1)) 168 | } 169 | /// Return the `Axis<3>` identical to the x-axis at the origin in reverse direction. 170 | pub fn neg_x() -> Self { 171 | Self::new(point!(0, 0, 0), dir!(-1, 0, 0)) 172 | } 173 | /// Return the `Axis<3>` identical to the y-axis at the origin in reverse direction. 174 | pub fn neg_y() -> Self { 175 | Self::new(point!(0, 0, 0), dir!(0, -1, 0)) 176 | } 177 | /// Return the `Axis<3>` identical to the z-axis at the origin in reverse direction. 178 | pub fn neg_z() -> Self { 179 | Self::new(point!(0, 0, 0), dir!(0, 0, -1)) 180 | } 181 | 182 | pub(crate) fn to_occt_ax1(self) -> UniquePtr { 183 | ffi::gp_Ax1_ctor(&self.origin.to_occt_point(), &self.direction.to_occt_dir()) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/core/dir.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, Mul, Sub}; 2 | 3 | use cxx::UniquePtr; 4 | use iter_fixed::IntoIteratorFixed; 5 | use opencascade_sys::ffi; 6 | 7 | use crate::{Angle, Error, Length, Point}; 8 | 9 | /// A direction in space with a length of 1. 10 | /// 11 | /// `Dir`s can be two- or three-dimensional. 12 | /// ```rust 13 | /// use anvil::Dir; 14 | /// 15 | /// let two_dimensional_dir = Dir::<2>::try_from([1., 2.]); 16 | /// let three_dimensional_dir = Dir::<3>::try_from([1., 2., 3.]); 17 | /// ``` 18 | /// 19 | /// The `dir!` macro can be used to simplify point construction. 20 | /// ```rust 21 | /// use anvil::{Dir, dir}; 22 | /// 23 | /// assert_eq!(dir!(3, 4), Dir::try_from([3., 4.]).unwrap()); 24 | /// assert_eq!(dir!(3, 4, 5), Dir::try_from([3., 4., 5.]).unwrap()); 25 | /// ``` 26 | #[derive(Debug, PartialEq, Copy, Clone, PartialOrd)] 27 | pub struct Dir([f64; DIM]); 28 | impl Dir { 29 | /// Construct a `Dir` from the directional components. 30 | /// 31 | /// Returns an Error::ZeroVector if all of the axis values are zero. 32 | /// 33 | /// ```rust 34 | /// use anvil::{Dir, Error}; 35 | /// 36 | /// // for 2d 37 | /// let dir2 = Dir::<2>::try_from([3., 4.]).unwrap(); 38 | /// assert_eq!(dir2.x(), 3. / 5.); 39 | /// assert_eq!(dir2.y(), 4. / 5.); 40 | /// assert_eq!(Dir::<2>::try_from([0., 0.]), Err(Error::ZeroVector)); 41 | /// 42 | /// // for 3d 43 | /// let dir3 = Dir::<3>::try_from([3., 0., 4.]).unwrap(); 44 | /// assert_eq!(dir3.x(), 3. / 5.); 45 | /// assert_eq!(dir3.y(), 0. / 5.); 46 | /// assert_eq!(dir3.z(), 4. / 5.); 47 | /// assert_eq!(Dir::<3>::try_from([0., 0., 0.]), Err(Error::ZeroVector)); 48 | /// ``` 49 | pub fn try_from(components: [f64; DIM]) -> Result { 50 | let magnitude = f64::sqrt(components.iter().map(|n| n.powi(2)).sum()); 51 | match magnitude { 52 | 0. => Err(Error::ZeroVector), 53 | _ => Ok(Self( 54 | components 55 | .into_iter_fixed() 56 | .map(|n| n / magnitude) 57 | .collect(), 58 | )), 59 | } 60 | } 61 | 62 | /// Return the dot-product of this `Dir` with another of the same dimension. 63 | pub fn dot(&self, other: Self) -> f64 { 64 | self.0.into_iter().zip(other.0).map(|(a, b)| a * b).sum() 65 | } 66 | 67 | /// Return true if this `Dir` has less than a 0.000001% difference to another. 68 | /// 69 | /// ```rust 70 | /// use anvil::dir; 71 | /// 72 | /// assert!(dir!(1, 1).approx_eq(dir!(1.00000001, 1))); 73 | /// assert!(!dir!(1, 1).approx_eq(dir!(0.5, 1))); 74 | /// ``` 75 | pub fn approx_eq(&self, other: Dir) -> bool { 76 | for (s, o) in self.0.iter().zip(other.0) { 77 | if (s / o - 1.).abs() > 0.0000001 { 78 | return false; 79 | } 80 | } 81 | true 82 | } 83 | } 84 | 85 | impl Dir<2> { 86 | /// Return the x-component of this `Dir<2>`. 87 | pub fn x(&self) -> f64 { 88 | self.0[0] 89 | } 90 | /// Return the y-component of this `Dir<2>`. 91 | pub fn y(&self) -> f64 { 92 | self.0[1] 93 | } 94 | 95 | /// Return the `Angle` this `Dir<2>` points to in relation to the unit circle. 96 | /// 97 | /// ```rust 98 | /// use anvil::{dir, IntoAngle}; 99 | /// 100 | /// assert!((dir!(1, 0).angle() - 0.deg()).rad().abs() < 1e-9); 101 | /// assert!((dir!(1, 1).angle() - 45.deg()).rad().abs() < 1e-9); 102 | /// assert!((dir!(0, 1).angle() - 90.deg()).rad().abs() < 1e-9); 103 | /// assert!((dir!(-1, 1).angle() - 135.deg()).rad().abs() < 1e-9); 104 | /// assert!((dir!(-1, 0).angle() - 180.deg()).rad().abs() < 1e-9); 105 | /// assert!((dir!(-1, -1).angle() - 225.deg()).rad().abs() < 1e-9); 106 | /// assert!((dir!(0, -1).angle() - 270.deg()).rad().abs() < 1e-9); 107 | /// assert!((dir!(1, -1).angle() - 315.deg()).rad().abs() < 1e-9); 108 | /// ``` 109 | pub fn angle(&self) -> Angle { 110 | let angle = Angle::from_rad(self.y().atan2(self.x())); 111 | if angle.rad() < 0. { 112 | Angle::from_rad(angle.rad() + std::f64::consts::TAU) 113 | } else { 114 | angle 115 | } 116 | } 117 | 118 | /// Return a `Dir<2>` rotated by a specified amount counter clockwise. 119 | pub fn rotate(&self, angle: Angle) -> Self { 120 | Self::from(self.angle() + angle) 121 | } 122 | } 123 | 124 | impl From for Dir<2> { 125 | /// Construct a `Dir<2>` from an `Angle`. 126 | /// 127 | /// An angle of 0 points in the positive x-direction and positive angles rotate counter 128 | /// clockwise. 129 | fn from(value: Angle) -> Self { 130 | Self([f64::cos(value.rad()), f64::sin(value.rad())]) 131 | } 132 | } 133 | 134 | impl Dir<3> { 135 | /// Return the x-component of this `Dir<3>`. 136 | pub fn x(&self) -> f64 { 137 | self.0[0] 138 | } 139 | /// Return the y-component of this `Dir<3>`. 140 | pub fn y(&self) -> f64 { 141 | self.0[1] 142 | } 143 | /// Return the z-component of this `Dir<3>`. 144 | pub fn z(&self) -> f64 { 145 | self.0[2] 146 | } 147 | 148 | /// Return the cross-product of this `Dir<3>` with another. 149 | pub fn cross(&self, other: Self) -> Self { 150 | Self([ 151 | self.y() * other.z() - self.z() * other.y(), 152 | self.z() * other.x() - self.x() * other.z(), 153 | self.x() * other.y() - self.y() * other.x(), 154 | ]) 155 | } 156 | 157 | pub(crate) fn to_occt_dir(self) -> UniquePtr { 158 | ffi::gp_Dir_ctor(self.x(), self.y(), self.z()) 159 | } 160 | } 161 | 162 | impl Add for Dir { 163 | type Output = Result; 164 | /// Add another `Dir` to this one. 165 | /// 166 | /// ```rust 167 | /// use anvil::{dir, Error}; 168 | /// 169 | /// // for 2d 170 | /// assert_eq!( 171 | /// dir!(0, 1) + dir!(1, 0), 172 | /// Ok(dir!(1, 1)) 173 | /// ); 174 | /// assert_eq!( 175 | /// dir!(1, 1) + dir!(-1, -1), 176 | /// Err(Error::ZeroVector) 177 | /// ); 178 | /// 179 | /// // for 3d 180 | /// assert_eq!( 181 | /// dir!(0, 1, 0) + dir!(1, 0, 0), 182 | /// Ok(dir!(1, 1, 0)) 183 | /// ); 184 | /// assert_eq!( 185 | /// dir!(1, 1, 1) + dir!(-1, -1, -1), 186 | /// Err(Error::ZeroVector) 187 | /// ); 188 | /// ``` 189 | fn add(self, other: Self) -> Result { 190 | Self::try_from( 191 | self.0 192 | .into_iter_fixed() 193 | .zip(other.0) 194 | .map(|(a, b)| a + b) 195 | .collect(), 196 | ) 197 | } 198 | } 199 | 200 | impl Sub for Dir { 201 | type Output = Result; 202 | /// Subtract another `Dir` from this one. 203 | /// 204 | /// ```rust 205 | /// use anvil::{dir, Error}; 206 | /// 207 | /// // for 2d 208 | /// assert_eq!( 209 | /// dir!(0, 1) - dir!(1, 0), 210 | /// Ok(dir!(-1, 1)) 211 | /// ); 212 | /// assert_eq!( 213 | /// dir!(1, 1) - dir!(1, 1), 214 | /// Err(Error::ZeroVector) 215 | /// ); 216 | /// 217 | /// // for 3d 218 | /// assert_eq!( 219 | /// dir!(0, 1, 0) - dir!(1, 0, 0), 220 | /// Ok(dir!(-1, 1, 0)) 221 | /// ); 222 | /// assert_eq!( 223 | /// dir!(1, 1, 1) - dir!(1, 1, 1), 224 | /// Err(Error::ZeroVector) 225 | /// ); 226 | /// ``` 227 | fn sub(self, other: Self) -> Result { 228 | Self::try_from( 229 | self.0 230 | .into_iter_fixed() 231 | .zip(other.0) 232 | .map(|(a, b)| a - b) 233 | .collect(), 234 | ) 235 | } 236 | } 237 | 238 | impl Mul for Dir { 239 | type Output = Point; 240 | /// Multiply this `Dir` with a `Length` to get a `Point`. 241 | /// 242 | /// ```rust 243 | /// use anvil::{IntoLength, dir, point}; 244 | /// 245 | /// // for 2d 246 | /// assert_eq!( 247 | /// dir!(1, 0) * 2.m(), 248 | /// point!(2.m(), 0.m()) 249 | /// ); 250 | /// 251 | /// // for 3d 252 | /// assert_eq!( 253 | /// dir!(1, 0, 0) * 2.m(), 254 | /// point!(2.m(), 0.m(), 0.m()) 255 | /// ); 256 | /// ``` 257 | fn mul(self, other: Length) -> Point { 258 | Point::new(self.0.into_iter_fixed().map(|n| n * other).collect()) 259 | } 260 | } 261 | 262 | /// Macro for simplifying `Dir` construction for static values. 263 | /// 264 | /// ```rust 265 | /// use anvil::{dir, Dir}; 266 | /// 267 | /// // for 2d 268 | /// assert_eq!(dir!(3, 4), Dir::try_from([3., 4.]).unwrap()); 269 | /// assert_eq!(dir!(3., 4.), Dir::try_from([3., 4.]).unwrap()); 270 | /// // dir!(0, 0); <- this raises a compile error 271 | /// 272 | /// // for 3d 273 | /// assert_eq!(dir!(3, 4, 5), Dir::try_from([3., 4., 5.]).unwrap()); 274 | /// assert_eq!(dir!(3., 4., 5.), Dir::try_from([3., 4., 5.]).unwrap()); 275 | /// // dir!(0, 0, 0); // <- this raises a compile error 276 | /// ``` 277 | #[macro_export] 278 | macro_rules! dir { 279 | ( 0., 0. ) => { 280 | compile_error!("At least one value of the Dir needs to be non-zero.") 281 | }; 282 | ( 0, 0. ) => { 283 | compile_error!("At least one value of the Dir needs to be non-zero.") 284 | }; 285 | ( 0., 0 ) => { 286 | compile_error!("At least one value of the Dir needs to be non-zero.") 287 | }; 288 | ( 0, 0 ) => { 289 | compile_error!("At least one value of the Dir needs to be non-zero.") 290 | }; 291 | ( $x:literal, $y:literal ) => { 292 | $crate::Dir::try_from([$x as f64, $y as f64]) 293 | .expect("macro already checked for zero values") 294 | }; 295 | 296 | ( 0., 0., 0. ) => { 297 | compile_error!("At least one value of the Dir needs to be non-zero.") 298 | }; 299 | ( 0, 0., 0. ) => { 300 | compile_error!("At least one value of the Dir needs to be non-zero.") 301 | }; 302 | ( 0, 0, 0. ) => { 303 | compile_error!("At least one value of the Dir needs to be non-zero.") 304 | }; 305 | ( 0, 0., 0 ) => { 306 | compile_error!("At least one value of the Dir needs to be non-zero.") 307 | }; 308 | ( 0., 0, 0. ) => { 309 | compile_error!("At least one value of the Dir needs to be non-zero.") 310 | }; 311 | ( 0., 0, 0 ) => { 312 | compile_error!("At least one value of the Dir needs to be non-zero.") 313 | }; 314 | ( 0., 0., 0 ) => { 315 | compile_error!("At least one value of the Dir needs to be non-zero.") 316 | }; 317 | ( 0, 0, 0 ) => { 318 | compile_error!("At least one value of the Dir needs to be non-zero.") 319 | }; 320 | ( $x:literal, $y:literal, $z:literal ) => { 321 | $crate::Dir::try_from([$x as f64, $y as f64, $z as f64]) 322 | .expect("macro already checked for zero values") 323 | }; 324 | } 325 | -------------------------------------------------------------------------------- /src/core/edge.rs: -------------------------------------------------------------------------------- 1 | use core::f64; 2 | 3 | use cxx::UniquePtr; 4 | use opencascade_sys::ffi; 5 | 6 | use crate::{Angle, Axis, Dir, Error, Length, Plane, Point}; 7 | 8 | /// A one-dimensional object in two-dimensional space. 9 | #[derive(Debug, PartialEq, Clone)] 10 | pub enum Edge { 11 | /// A circle section defined by the start point, a mid point and the end point. 12 | Arc(Point<2>, Point<2>, Point<2>), 13 | 14 | /// A line between two points. 15 | Line(Point<2>, Point<2>), 16 | } 17 | impl Edge { 18 | /// Return the starting point of the edge. 19 | /// 20 | /// ```rust 21 | /// use anvil::{Edge, IntoLength, point}; 22 | /// 23 | /// let edge = Edge::Line(point!(1.m(), 1.m()), point!(2.m(), 2.m())); 24 | /// assert_eq!(edge.start(), point!(1.m(), 1.m())) 25 | /// ``` 26 | pub fn start(&self) -> Point<2> { 27 | match self { 28 | Self::Arc(start, _, _) => *start, 29 | Self::Line(start, _) => *start, 30 | } 31 | } 32 | /// Return the ending point of the edge. 33 | /// 34 | /// ```rust 35 | /// use anvil::{Edge, IntoLength, point}; 36 | /// 37 | /// let edge = Edge::Line(point!(1.m(), 1.m()), point!(2.m(), 2.m())); 38 | /// assert_eq!(edge.end(), point!(2.m(), 2.m())) 39 | /// ``` 40 | pub fn end(&self) -> Point<2> { 41 | match self { 42 | Self::Arc(_, _, end) => *end, 43 | Self::Line(_, end) => *end, 44 | } 45 | } 46 | 47 | /// Return the distance spanned by the `Edge`. 48 | /// 49 | /// ```rust 50 | /// use core::f64; 51 | /// use anvil::{Edge, IntoLength, point}; 52 | /// 53 | /// let line = Edge::Line(point!(1.m(), 0.m()), point!(1.m(), 2.m())); 54 | /// assert_eq!(line.len(), 2.m()); 55 | /// 56 | /// let arc = Edge::Arc(point!(-1.m(), 0.m()), point!(0.m(), 1.m()), point!(1.m(), 0.m())); 57 | /// assert_eq!(arc.len(), f64::consts::PI.m()); 58 | /// ``` 59 | pub fn len(&self) -> Length { 60 | match self { 61 | Self::Arc(start, mid, end) => { 62 | // Works for now but needs to be refactored in the future 63 | let (x1, y1) = (start.x().m(), start.y().m()); 64 | let (x2, y2) = (mid.x().m(), mid.y().m()); 65 | let (x3, y3) = (end.x().m(), end.y().m()); 66 | 67 | let b = (x1.powi(2) + y1.powi(2)) * (y3 - y2) 68 | + (x2.powi(2) + y2.powi(2)) * (y1 - y3) 69 | + (x3.powi(2) + y3.powi(2)) * (y2 - y1); 70 | let c = (x1.powi(2) + y1.powi(2)) * (x2 - x3) 71 | + (x2.powi(2) + y2.powi(2)) * (x3 - x1) 72 | + (x3.powi(2) + y3.powi(2)) * (x1 - x2); 73 | 74 | let denom = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)); 75 | if denom.abs() < f64::EPSILON { 76 | return Length::zero(); 77 | } 78 | let cx = -b / denom; 79 | let cy = -c / denom; 80 | 81 | let r = f64::sqrt((x1 - cx).powi(2) + (y1 - cy).powi(2)); 82 | 83 | let v1 = ((x1 - cx), (y1 - cy)); 84 | let v2 = ((x3 - cx), (y3 - cy)); 85 | 86 | let dot = v1.0 * v2.0 + v1.1 * v2.1; 87 | let det = v1.0 * v2.1 - v1.1 * v2.0; 88 | let mut angle = det.atan2(dot).abs(); 89 | 90 | let is_mid_on_arc = { 91 | let cross1 = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1); 92 | let cross2 = (cx - x1) * (y3 - y1) - (cy - y1) * (x3 - x1); 93 | cross1 * cross2 >= 0.0 94 | }; 95 | if !is_mid_on_arc { 96 | angle = f64::consts::TAU - angle; 97 | } 98 | 99 | Length::from_m(r * angle) 100 | } 101 | Self::Line(start, end) => { 102 | let diff = *start - *end; 103 | Length::from_m(f64::sqrt(diff.x().m().powi(2) + diff.y().m().powi(2))) 104 | } 105 | } 106 | } 107 | 108 | /// Return the direction this `Edge` is pointing to at its end point. 109 | /// 110 | /// ```rust 111 | /// use anvil::{Edge, IntoLength, dir, point}; 112 | /// 113 | /// let line = Edge::Line(point!(0, 0), point!(1.m(), 2.m())); 114 | /// assert_eq!(line.end_direction(), Ok(dir!(1, 2))); 115 | /// ``` 116 | pub fn end_direction(&self) -> Result, Error> { 117 | match self { 118 | Self::Arc(start, interior, end) => { 119 | let (center, _) = arc_center_radius(*start, *interior, *end)?; 120 | 121 | let start_angle = arc_point_angle_on_unit_circle(center, *start); 122 | let interior_angle = arc_point_angle_on_unit_circle(center, *interior); 123 | let end_angle = arc_point_angle_on_unit_circle(center, *end); 124 | 125 | let arc_is_clockwise = (end_angle > start_angle || start_angle > interior_angle) 126 | && interior_angle > end_angle; 127 | 128 | if arc_is_clockwise { 129 | Ok(Dir::from(end_angle - Angle::from_deg(90.))) 130 | } else { 131 | Ok(Dir::from(end_angle + Angle::from_deg(90.))) 132 | } 133 | } 134 | Self::Line(start, end) => { 135 | Dir::<2>::try_from([(*end - *start).x().m(), (*end - *start).y().m()]) 136 | } 137 | } 138 | } 139 | 140 | pub(crate) fn to_occt(&self, plane: Plane) -> Option> { 141 | if self.len() == Length::zero() { 142 | return None; 143 | } 144 | match self { 145 | Self::Arc(start, mid, end) => { 146 | let make_arc = ffi::GC_MakeArcOfCircle_point_point_point( 147 | &start.to_3d(plane).to_occt_point(), 148 | &mid.to_3d(plane).to_occt_point(), 149 | &end.to_3d(plane).to_occt_point(), 150 | ); 151 | Some(ffi::TopoDS_Edge_to_owned( 152 | ffi::BRepBuilderAPI_MakeEdge_HandleGeomCurve( 153 | &ffi::new_HandleGeomCurve_from_HandleGeom_TrimmedCurve( 154 | &ffi::GC_MakeArcOfCircle_Value(&make_arc), 155 | ), 156 | ) 157 | .pin_mut() 158 | .Edge(), 159 | )) 160 | } 161 | Self::Line(start, end) => { 162 | let mut constructor = ffi::BRepBuilderAPI_MakeEdge_gp_Pnt_gp_Pnt( 163 | &start.to_3d(plane).to_occt_point(), 164 | &end.to_3d(plane).to_occt_point(), 165 | ); 166 | Some(ffi::TopoDS_Edge_to_owned(constructor.pin_mut().Edge())) 167 | } 168 | } 169 | } 170 | } 171 | 172 | fn arc_center_radius( 173 | start: Point<2>, 174 | interior: Point<2>, 175 | end: Point<2>, 176 | ) -> Result<(Point<2>, Length), Error> { 177 | if start == interior || interior == end || end == start { 178 | return Err(Error::ZeroVector); 179 | } 180 | 181 | let start_interior_midpoint = Point::<2>::new([ 182 | (start.x() + interior.x()) / 2., 183 | (start.y() + interior.y()) / 2., 184 | ]); 185 | let interior_end_midpoint = 186 | Point::<2>::new([(end.x() + interior.x()) / 2., (end.y() + interior.y()) / 2.]); 187 | 188 | let start_interior_direction = interior 189 | .direction_from(start) 190 | .expect("zero vector already checked above"); 191 | let interior_end_direction = end 192 | .direction_from(interior) 193 | .expect("zero vector already checked above"); 194 | 195 | let start_interior_axis = Axis::<2>::new( 196 | start_interior_midpoint, 197 | start_interior_direction.rotate(Angle::from_deg(90.)), 198 | ); 199 | let interior_end_axis = Axis::<2>::new( 200 | interior_end_midpoint, 201 | interior_end_direction.rotate(Angle::from_deg(90.)), 202 | ); 203 | 204 | let center = start_interior_axis 205 | .intersect(interior_end_axis) 206 | .expect("zero vector already checked above"); 207 | 208 | let radius = (center - start).distance_to(Point::<2>::origin()); 209 | 210 | Ok((center, radius)) 211 | } 212 | 213 | fn arc_point_angle_on_unit_circle(center: Point<2>, point: Point<2>) -> Angle { 214 | point 215 | .direction_from(center) 216 | .expect("center and point can not be the same") 217 | .angle() 218 | } 219 | -------------------------------------------------------------------------------- /src/core/intof64.rs: -------------------------------------------------------------------------------- 1 | /// Convert any number into an f64. 2 | pub trait IntoF64 { 3 | /// Convert this number into an f64 4 | fn to_f64(&self) -> f64; 5 | } 6 | macro_rules! impl_into_f64 { 7 | ($t:tt) => { 8 | impl IntoF64 for $t { 9 | fn to_f64(&self) -> f64 { 10 | *self as f64 11 | } 12 | } 13 | }; 14 | } 15 | 16 | impl_into_f64!(usize); 17 | impl_into_f64!(isize); 18 | impl_into_f64!(u8); 19 | impl_into_f64!(u16); 20 | impl_into_f64!(u32); 21 | impl_into_f64!(u64); 22 | impl_into_f64!(u128); 23 | impl_into_f64!(i8); 24 | impl_into_f64!(i16); 25 | impl_into_f64!(i32); 26 | impl_into_f64!(i64); 27 | impl_into_f64!(i128); 28 | impl_into_f64!(f32); 29 | impl_into_f64!(f64); 30 | -------------------------------------------------------------------------------- /src/core/length.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Debug, 3 | ops::{Add, Div, Mul, Neg, Sub}, 4 | }; 5 | 6 | use crate::{Dir, IntoF64, Point}; 7 | 8 | /// A physical length (i.e. a distance). 9 | /// 10 | /// Length exists to remove ambiguity about distance units, which are not supported by default by 11 | /// major CAD kernels. 12 | /// 13 | /// ```rust 14 | /// use anvil::Length; 15 | /// 16 | /// // You can construct a `Length` using the Length::from_[unit] methods like 17 | /// let meters_length = Length::from_m(1.2); 18 | /// let centimeters_length = Length::from_cm(4.5); 19 | /// let inches_length = Length::from_in(12.); 20 | /// 21 | /// // To get back a `Length` value in a specific unit, call the Length.[unit] method 22 | /// assert_eq!(meters_length.cm(), 120.); 23 | /// assert_eq!(centimeters_length.m(), 0.045); 24 | /// assert!((inches_length.ft() - 1.).abs() < 1e-9); 25 | /// 26 | /// // Length construction can be simplified using the `IntoLength` trait. 27 | /// use anvil::IntoLength; 28 | /// 29 | /// assert_eq!(1.2.m(), Length::from_m(1.2)); 30 | /// assert_eq!(4.5.cm(), Length::from_cm(4.5)); 31 | /// assert_eq!(12.in_(), Length::from_in(12.)); 32 | /// ``` 33 | #[derive(PartialEq, Copy, Clone, PartialOrd)] 34 | pub struct Length { 35 | meters: f64, 36 | } 37 | impl Length { 38 | /// Construct a `Length` with a value of zero. 39 | /// 40 | /// # Example 41 | /// ```rust 42 | /// use anvil::Length; 43 | /// 44 | /// let len = Length::zero(); 45 | /// assert_eq!(len.m(), 0.); 46 | /// ``` 47 | pub const fn zero() -> Self { 48 | Self { meters: 0. } 49 | } 50 | /// Construct a `Length` from a value of unit meters. 51 | /// 52 | /// # Example 53 | /// ```rust 54 | /// use anvil::Length; 55 | /// 56 | /// let len = Length::from_m(3.2); 57 | /// assert_eq!(len.mm(), 3200.); 58 | /// ``` 59 | pub const fn from_m(value: f64) -> Self { 60 | Self { meters: value } 61 | } 62 | /// Return the value of this `Length` in millimeters. 63 | pub const fn m(&self) -> f64 { 64 | self.meters 65 | } 66 | /// Construct a `Length` from a value of unit yards. 67 | /// 68 | /// # Example 69 | /// ```rust 70 | /// use anvil::Length; 71 | /// 72 | /// let len = Length::from_yd(1.); 73 | /// assert_eq!(len.m(), 0.9144); 74 | /// ``` 75 | pub const fn from_yd(value: f64) -> Self { 76 | Self::from_m(value * 0.9144) 77 | } 78 | /// Return the value of this `Length` in yards. 79 | pub const fn yd(&self) -> f64 { 80 | self.m() / 0.9144 81 | } 82 | /// Construct a `Length` from a value of unit feet. 83 | /// 84 | /// # Example 85 | /// ```rust 86 | /// use anvil::Length; 87 | /// 88 | /// let len = Length::from_ft(1.); 89 | /// assert_eq!(len.cm(), 30.48); 90 | /// ``` 91 | pub const fn from_ft(value: f64) -> Self { 92 | Self::from_m(value * 0.3048) 93 | } 94 | /// Return the value of this `Length` in feet. 95 | pub const fn ft(&self) -> f64 { 96 | self.m() / 0.3048 97 | } 98 | /// Construct a `Length` from a value of unit decimeters. 99 | /// 100 | /// # Example 101 | /// ```rust 102 | /// use anvil::Length; 103 | /// 104 | /// let len = Length::from_dm(5.1); 105 | /// assert_eq!(len.mm(), 510.); 106 | /// ``` 107 | pub const fn from_dm(value: f64) -> Self { 108 | Self::from_m(value / 10.) 109 | } 110 | /// Return the value of this `Length` in decimeters. 111 | pub const fn dm(&self) -> f64 { 112 | self.m() * 10. 113 | } 114 | /// Construct a `Length` from a value of unit inches. 115 | /// 116 | /// # Example 117 | /// ```rust 118 | /// use anvil::Length; 119 | /// 120 | /// let len = Length::from_in(1.); 121 | /// assert_eq!(len.cm(), 2.54); 122 | /// ``` 123 | pub const fn from_in(value: f64) -> Self { 124 | Self::from_m(value * 0.0254) 125 | } 126 | /// Return the value of this `Length` in inches. 127 | /// 128 | /// This method breaks the pattern with the trailing underscore, because `in` is a reserved 129 | /// keyword in Rust. 130 | pub const fn in_(&self) -> f64 { 131 | self.m() / 0.0254 132 | } 133 | /// Construct a `Length` from a value of unit centimeters. 134 | /// 135 | /// # Example 136 | /// ```rust 137 | /// use anvil::Length; 138 | /// 139 | /// let len = Length::from_cm(5.1); 140 | /// assert_eq!(len.mm(), 51.); 141 | /// ``` 142 | pub const fn from_cm(value: f64) -> Self { 143 | Self::from_m(value / 100.) 144 | } 145 | /// Return the value of this `Length` in centimeters. 146 | pub const fn cm(&self) -> f64 { 147 | self.m() * 100. 148 | } 149 | /// Construct a `Length` from a value of unit millimeters. 150 | /// 151 | /// # Example 152 | /// ```rust 153 | /// use anvil::Length; 154 | /// 155 | /// let len = Length::from_mm(5.4); 156 | /// assert_eq!(len.m(), 0.0054); 157 | /// ``` 158 | pub const fn from_mm(value: f64) -> Self { 159 | Self::from_m(value / 1000.) 160 | } 161 | /// Return the value of this `Length` in millimeters. 162 | pub const fn mm(&self) -> f64 { 163 | self.m() * 1000. 164 | } 165 | 166 | /// Return the absolute value of this `Length`. 167 | /// 168 | /// ```rust 169 | /// use anvil::IntoLength; 170 | /// 171 | /// assert_eq!((-5).m().abs(), 5.m()); 172 | /// assert_eq!(5.m().abs(), 5.m()); 173 | /// ``` 174 | pub const fn abs(&self) -> Self { 175 | Self { 176 | meters: self.meters.abs(), 177 | } 178 | } 179 | /// Return the smaller of two lengths. 180 | /// 181 | /// # Example 182 | /// ```rust 183 | /// use anvil::IntoLength; 184 | /// 185 | /// let len1 = 1.m(); 186 | /// let len2 = 2.m(); 187 | /// assert_eq!(len1.min(&len2), len1); 188 | /// assert_eq!(len2.min(&len1), len1); 189 | /// ``` 190 | pub const fn min(&self, other: &Self) -> Self { 191 | Length::from_m(self.m().min(other.m())) 192 | } 193 | /// Return the larger of two lengths. 194 | /// 195 | /// # Example 196 | /// ```rust 197 | /// use anvil::IntoLength; 198 | /// 199 | /// let len1 = 1.m(); 200 | /// let len2 = 2.m(); 201 | /// assert_eq!(len1.max(&len2), len2); 202 | /// assert_eq!(len2.max(&len1), len2); 203 | /// ``` 204 | pub const fn max(&self, other: &Self) -> Self { 205 | Length::from_m(self.m().max(other.m())) 206 | } 207 | } 208 | impl Debug for Length { 209 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 210 | f.write_str(format!("{}m", self.m()).as_str()) 211 | } 212 | } 213 | 214 | impl Add for Length { 215 | type Output = Length; 216 | fn add(self, other: Length) -> Length { 217 | Length::from_m(self.m() + other.m()) 218 | } 219 | } 220 | 221 | impl Sub for Length { 222 | type Output = Length; 223 | fn sub(self, other: Length) -> Length { 224 | Length::from_m(self.m() - other.m()) 225 | } 226 | } 227 | 228 | impl Mul for Length { 229 | type Output = Length; 230 | fn mul(self, other: f64) -> Length { 231 | Length::from_m(self.m() * other) 232 | } 233 | } 234 | 235 | impl Mul for f64 { 236 | type Output = Length; 237 | fn mul(self, other: Length) -> Length { 238 | other * self 239 | } 240 | } 241 | 242 | impl Div for Length { 243 | type Output = Length; 244 | fn div(self, other: f64) -> Length { 245 | Length::from_m(self.m() / other) 246 | } 247 | } 248 | 249 | impl Div for Length { 250 | type Output = f64; 251 | /// Divide this `Length` by another `Length`. 252 | /// ```rust 253 | /// use anvil::IntoLength; 254 | /// 255 | /// assert_eq!(6.m() / 2.m(), 3.) 256 | /// ``` 257 | fn div(self, other: Length) -> f64 { 258 | self.meters / other.meters 259 | } 260 | } 261 | 262 | impl Mul> for Length { 263 | type Output = Point; 264 | /// Multiply this `Length` with a `Dir` to get a `Point`. 265 | /// 266 | /// ```rust 267 | /// use anvil::{IntoLength, dir, point}; 268 | /// 269 | /// // for 2d 270 | /// assert_eq!( 271 | /// 2.m() * dir!(1, 0), 272 | /// point!(2.m(), 0.m()) 273 | /// ); 274 | /// 275 | /// // for 3d 276 | /// assert_eq!( 277 | /// 2.m() * dir!(1, 0, 0), 278 | /// point!(2.m(), 0.m(), 0.m()) 279 | /// ); 280 | /// ``` 281 | fn mul(self, other: Dir) -> Point { 282 | other * self 283 | } 284 | } 285 | 286 | impl Neg for Length { 287 | type Output = Length; 288 | fn neg(self) -> Self::Output { 289 | self * -1. 290 | } 291 | } 292 | 293 | /// Return true if any IntoLength in the input array is zero. 294 | pub fn is_zero(lengths: &[Length]) -> bool { 295 | for length in lengths { 296 | if length.m() == 0. { 297 | return true; 298 | } 299 | } 300 | false 301 | } 302 | 303 | /// Import this trait to easily convert numbers into `Length`s. 304 | /// 305 | /// ```rust 306 | /// use anvil::{IntoLength, Length}; 307 | /// 308 | /// assert_eq!(5.m(), Length::from_m(5.)); 309 | /// assert_eq!(5.123.ft(), Length::from_ft(5.123)); 310 | /// ``` 311 | pub trait IntoLength: IntoF64 { 312 | /// Convert this number into a `Length` in yard. 313 | /// 314 | /// ```rust 315 | /// use anvil::{IntoLength, Length}; 316 | /// 317 | /// assert_eq!(5.yd(), Length::from_yd(5.)); 318 | /// ``` 319 | fn yd(&self) -> Length { 320 | Length::from_yd(self.to_f64()) 321 | } 322 | /// Convert this number into a `Length` in meters. 323 | /// 324 | /// ```rust 325 | /// use anvil::{IntoLength, Length}; 326 | /// 327 | /// assert_eq!(5.m(), Length::from_m(5.)); 328 | /// ``` 329 | fn m(&self) -> Length { 330 | Length::from_m(self.to_f64()) 331 | } 332 | /// Convert this number into a `Length` in feet. 333 | /// 334 | /// ```rust 335 | /// use anvil::{IntoLength, Length}; 336 | /// 337 | /// assert_eq!(5.ft(), Length::from_ft(5.)); 338 | /// ``` 339 | fn ft(&self) -> Length { 340 | Length::from_ft(self.to_f64()) 341 | } 342 | /// Convert this number into a `Length` in decimeters. 343 | /// 344 | /// ```rust 345 | /// use anvil::{IntoLength, Length}; 346 | /// 347 | /// assert_eq!(5.dm(), Length::from_dm(5.)); 348 | /// ``` 349 | fn dm(&self) -> Length { 350 | Length::from_dm(self.to_f64()) 351 | } 352 | /// Convert this number into a `Length` in inches. 353 | /// 354 | /// ```rust 355 | /// use anvil::{IntoLength, Length}; 356 | /// 357 | /// assert_eq!(5.in_(), Length::from_in(5.)); 358 | /// ``` 359 | fn in_(&self) -> Length { 360 | Length::from_in(self.to_f64()) 361 | } 362 | /// Convert this number into a `Length` in centimeters. 363 | /// 364 | /// ```rust 365 | /// use anvil::{IntoLength, Length}; 366 | /// 367 | /// assert_eq!(5.cm(), Length::from_cm(5.)); 368 | /// ``` 369 | fn cm(&self) -> Length { 370 | Length::from_cm(self.to_f64()) 371 | } 372 | /// Convert this number into a `Length` in millimeters. 373 | /// 374 | /// ```rust 375 | /// use anvil::{IntoLength, Length}; 376 | /// 377 | /// assert_eq!(5.mm(), Length::from_mm(5.)); 378 | /// ``` 379 | fn mm(&self) -> Length { 380 | Length::from_mm(self.to_f64()) 381 | } 382 | } 383 | 384 | impl IntoLength for usize {} 385 | impl IntoLength for isize {} 386 | impl IntoLength for u8 {} 387 | impl IntoLength for u16 {} 388 | impl IntoLength for u32 {} 389 | impl IntoLength for u64 {} 390 | impl IntoLength for u128 {} 391 | impl IntoLength for i8 {} 392 | impl IntoLength for i16 {} 393 | impl IntoLength for i32 {} 394 | impl IntoLength for i64 {} 395 | impl IntoLength for i128 {} 396 | impl IntoLength for f32 {} 397 | impl IntoLength for f64 {} 398 | 399 | #[cfg(test)] 400 | mod tests { 401 | use super::*; 402 | 403 | #[test] 404 | fn add() { 405 | assert_eq!(2.m() + 3.m(), 5.m()); 406 | } 407 | 408 | #[test] 409 | fn subtract() { 410 | assert_eq!(3.m() - 2.m(), 1.m()); 411 | } 412 | 413 | #[test] 414 | fn multiply_with_f64() { 415 | assert_eq!(5.m() * 4., Length::from_m(20.)); 416 | assert_eq!(4. * 5.m(), Length::from_m(20.)); 417 | } 418 | 419 | #[test] 420 | fn divide_with_f64() { 421 | assert_eq!(Length::from_m(6.) / 2., 3.m()); 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod angle; 2 | mod axis; 3 | mod dir; 4 | mod edge; 5 | mod intof64; 6 | mod length; 7 | mod path; 8 | mod plane; 9 | mod point; 10 | 11 | pub use angle::{Angle, IntoAngle}; 12 | pub use axis::Axis; 13 | pub use dir::Dir; 14 | pub use edge::Edge; 15 | pub use intof64::IntoF64; 16 | pub use length::{IntoLength, Length, is_zero}; 17 | pub use path::Path; 18 | pub use plane::Plane; 19 | pub use point::Point; 20 | -------------------------------------------------------------------------------- /src/core/path.rs: -------------------------------------------------------------------------------- 1 | use crate::{Angle, Axis, Dir, Edge, Length, Point, Sketch}; 2 | 3 | /// A continuous series of edges (i.e. lines, arcs, ...). 4 | #[derive(Debug, PartialEq, Clone)] 5 | pub struct Path { 6 | cursor: Point<2>, 7 | edges: Vec, 8 | } 9 | impl Path { 10 | /// Construct an empty `Path` at a given starting point. 11 | /// 12 | /// ```rust 13 | /// use anvil::{IntoLength, Path, point}; 14 | /// 15 | /// let path = Path::at(point!(1.m(), 2.m())); 16 | /// assert_eq!(path.start(), point!(1.m(), 2.m())) 17 | /// ``` 18 | pub fn at(start: Point<2>) -> Self { 19 | Self { 20 | cursor: start, 21 | edges: vec![], 22 | } 23 | } 24 | 25 | /// Add a line to the end of this `Path` that ends at a specified point. 26 | /// 27 | /// ```rust 28 | /// use anvil::{IntoLength, Path, point}; 29 | /// 30 | /// let path = Path::at(point!(1.m(), 2.m())).line_to(point!(3.m(), 4.m())); 31 | /// assert_eq!(path.end(), point!(3.m(), 4.m())) 32 | /// ``` 33 | pub fn line_to(&self, point: Point<2>) -> Self { 34 | self.add_edge(Edge::Line(self.cursor, point)) 35 | } 36 | 37 | /// Add a line to the end of this `Path` that extends by a specified amount in x and y direction. 38 | /// 39 | /// ```rust 40 | /// use anvil::{IntoLength, Path, point}; 41 | /// 42 | /// let path = Path::at(point!(1.m(), 2.m())).line_by(3.m(), 4.m()); 43 | /// assert_eq!(path.end(), point!(4.m(), 6.m())) 44 | /// ``` 45 | pub fn line_by(&self, dx: Length, dy: Length) -> Self { 46 | self.add_edge(Edge::Line( 47 | self.cursor, 48 | self.cursor + Point::<2>::new([dx, dy]), 49 | )) 50 | } 51 | 52 | /// Append a circle section to this `Path` that curves the Path by a certain angle. 53 | /// 54 | /// A positive radius curves the path to the left and a negative radius to the right. A positive 55 | /// angle ensures the path is smooth while a negative angle creates a sharp corner in the other 56 | /// direction. 57 | /// 58 | /// If the path was empty before, the arc starts in positive x-direction. 59 | /// 60 | /// ```rust 61 | /// use anvil::{Circle, IntoAngle, IntoLength, Edge, Path, point, Rectangle, Plane}; 62 | /// 63 | /// let sketch = Path::at(point!(1.m(), 1.m())) 64 | /// .arc_by(-1.m(), 180.deg()) 65 | /// .line_by(-2.m(), 0.m()) 66 | /// .arc_by(-1.m(), 30.deg()) 67 | /// .arc_by(-1.m(), 150.deg()) // arcs can be split into sections 68 | /// .close(); 69 | /// 70 | /// assert_eq!( 71 | /// sketch, 72 | /// Rectangle::from_dim(2.m(), 2.m()) 73 | /// .add(&Circle::from_radius(1.m()).move_to(point!(1.m(), 0.m()))) 74 | /// .add(&Circle::from_radius(1.m()).move_to(point!(-1.m(), 0.m()))) 75 | /// ) 76 | /// ``` 77 | pub fn arc_by(&self, radius: Length, angle: Angle) -> Self { 78 | if radius == Length::zero() || angle == Angle::zero() { 79 | return self.clone(); 80 | } 81 | let center = self.cursor + self.end_direction().rotate(Angle::from_deg(90.)) * radius; 82 | let center_cursor_axis = 83 | Axis::<2>::between(center, self.cursor).expect("zero radius already checked"); 84 | let direction_factor = radius / radius.abs(); 85 | 86 | let interim_point = center 87 | + center_cursor_axis 88 | .direction 89 | .rotate(angle / 2. * direction_factor) 90 | * radius.abs(); 91 | let end_point = center 92 | + center_cursor_axis 93 | .direction 94 | .rotate(angle * direction_factor) 95 | * radius.abs(); 96 | 97 | self.add_edge(Edge::Arc(self.cursor, interim_point, end_point)) 98 | } 99 | 100 | /// Add a circle section to the end of this `Path` two points. 101 | /// 102 | /// ```rust 103 | /// use anvil::{Edge, IntoLength, Path, point}; 104 | /// 105 | /// let path = Path::at(point!(0, 0)).arc_points(point!(1.m(), 1.m()), point!(0.m(), 2.m())); 106 | /// assert_eq!(path.cursor(), point!(0.m(), 2.m())); 107 | /// assert_eq!(path.edges(), vec![Edge::Arc(point!(0, 0), point!(1.m(), 1.m()), point!(0.m(), 2.m()))]); 108 | /// ``` 109 | pub fn arc_points(&self, mid: Point<2>, end: Point<2>) -> Self { 110 | self.add_edge(Edge::Arc(self.cursor, mid, end)) 111 | } 112 | 113 | /// Connect the end of this `Path` to its start with a straight line and return the resulting `Sketch`. 114 | pub fn close(self) -> Sketch { 115 | if self.start() == self.end() { 116 | Sketch::from_edges(self.edges) 117 | } else { 118 | Sketch::from_edges(self.line_to(self.start()).edges) 119 | } 120 | } 121 | 122 | /// Return the starting point of the `Path`. 123 | /// 124 | /// If the path does not have any edges, the cursor is returned. 125 | /// 126 | /// ```rust 127 | /// use anvil::{IntoLength, Path, point}; 128 | /// 129 | /// let path = Path::at(point!(1.m(), 2.m())).line_to(point!(3.m(), 4.m())); 130 | /// assert_eq!(path.start(), point!(1.m(), 2.m())); 131 | /// 132 | /// let empty_path = Path::at(point!(5.m(), 6.m())); 133 | /// assert_eq!(empty_path.start(), point!(5.m(), 6.m())); 134 | /// ``` 135 | pub fn start(&self) -> Point<2> { 136 | match self.edges.first() { 137 | Some(edge) => edge.start(), 138 | None => self.cursor, 139 | } 140 | } 141 | 142 | /// Return the ending point of the `Path`. 143 | /// 144 | /// If the path does not have any edges, the cursor is returned. 145 | /// 146 | /// ```rust 147 | /// use anvil::{IntoLength, Path, point}; 148 | /// 149 | /// let path = Path::at(point!(1.m(), 2.m())).line_to(point!(3.m(), 4.m())); 150 | /// assert_eq!(path.end(), point!(3.m(), 4.m())); 151 | /// 152 | /// let empty_path = Path::at(point!(5.m(), 6.m())); 153 | /// assert_eq!(empty_path.end(), point!(5.m(), 6.m())); 154 | /// ``` 155 | pub fn end(&self) -> Point<2> { 156 | match self.edges.iter().last() { 157 | Some(edge) => edge.end(), 158 | None => self.cursor, 159 | } 160 | } 161 | 162 | /// Return the direction the last element of this `Path` is pointing to. 163 | /// 164 | /// If the path is empty, a `Dir` parallel to the positive x-direction is returned. 165 | /// 166 | /// ```rust 167 | /// use anvil::{IntoLength, Path, dir, point}; 168 | /// 169 | /// assert_eq!( 170 | /// Path::at(point!(0, 0)).line_to(point!(0.m(), 2.m())).end_direction(), 171 | /// dir!(0, 1) 172 | /// ); 173 | /// assert_eq!( 174 | /// Path::at(point!(0, 0)).end_direction(), 175 | /// dir!(1, 0) 176 | /// ) 177 | /// ``` 178 | pub fn end_direction(&self) -> Dir<2> { 179 | match self.edges.last() { 180 | Some(last_edge) => last_edge 181 | .end_direction() 182 | .expect("edge has already been checked for zero length"), 183 | None => Dir::from(Angle::zero()), 184 | } 185 | } 186 | 187 | /// Return the edges in this `Path`. 188 | pub fn edges(&self) -> Vec { 189 | self.edges.clone() 190 | } 191 | 192 | /// Return the current cursor position of this `Path`. 193 | pub fn cursor(&self) -> Point<2> { 194 | self.cursor 195 | } 196 | 197 | fn add_edge(&self, edge: Edge) -> Self { 198 | if edge.start() != self.end() { 199 | panic!("path is not continuous"); 200 | } 201 | 202 | let new_cursor = edge.end(); 203 | let mut new_edges = self.edges.clone(); 204 | new_edges.push(edge); 205 | 206 | Self { 207 | cursor: new_cursor, 208 | edges: new_edges, 209 | } 210 | } 211 | } 212 | 213 | #[cfg(test)] 214 | mod tests { 215 | use assert_float_eq::assert_float_relative_eq; 216 | 217 | use super::*; 218 | use crate::{IntoAngle, IntoLength, dir, point}; 219 | 220 | fn assert_dir_eq(dir1: Dir<2>, dir2: Dir<2>) { 221 | assert_float_relative_eq!(dir1.x(), dir2.x()); 222 | assert_float_relative_eq!(dir1.y(), dir2.y()); 223 | } 224 | 225 | fn assert_point_eq(point1: Point<2>, point2: Point<2>) { 226 | assert_float_relative_eq!(point1.x().m(), point2.x().m()); 227 | assert_float_relative_eq!(point1.y().m(), point2.y().m()); 228 | } 229 | 230 | #[test] 231 | fn end_arc_positive_radius_angle() { 232 | let path = Path::at(point!(0, 0)).arc_by(1.m(), 90.deg()); 233 | assert_point_eq(path.end(), point!(1.m(), 1.m())) 234 | } 235 | 236 | #[test] 237 | fn end_arc_positive_radius_negative_angle() { 238 | let path = Path::at(point!(0, 0)).arc_by(1.m(), -90.deg()); 239 | assert_point_eq(path.end(), point!(-1.m(), 1.m())) 240 | } 241 | 242 | #[test] 243 | fn end_arc_negative_radius_positive_angle() { 244 | let path = Path::at(point!(0, 0)).arc_by(-1.m(), 90.deg()); 245 | assert_point_eq(path.end(), point!(1.m(), -1.m())) 246 | } 247 | 248 | #[test] 249 | fn end_arc_negative_radius_angle() { 250 | let path = Path::at(point!(0, 0)).arc_by(-1.m(), -90.deg()); 251 | assert_point_eq(path.end(), point!(-1.m(), -1.m())) 252 | } 253 | 254 | #[test] 255 | fn end_arc_negative_radius_positive_angle_45deg() { 256 | let path = Path::at(point!(0.m(), 1.m())).arc_by(-1.m(), 45.deg()); 257 | assert_point_eq( 258 | path.end(), 259 | point!(1.m() / f64::sqrt(2.), 1.m() / f64::sqrt(2.)), 260 | ) 261 | } 262 | 263 | #[test] 264 | fn end_direction_empty_path() { 265 | let path = Path::at(point!(0, 0)); 266 | assert_dir_eq(path.end_direction(), dir!(1, 0)) 267 | } 268 | 269 | #[test] 270 | fn end_direction_line() { 271 | let path = Path::at(point!(0, 0)).line_to(point!(1.m(), 1.m())); 272 | assert_dir_eq(path.end_direction(), dir!(1, 1)) 273 | } 274 | 275 | #[test] 276 | fn end_direction_arc_positive_radius_angle() { 277 | let path = Path::at(point!(0, 0)).arc_by(1.m(), 45.deg()); 278 | assert_dir_eq(path.end_direction(), dir!(1, 1)) 279 | } 280 | 281 | #[test] 282 | fn end_direction_arc_positive_radius_negative_angle() { 283 | let path = Path::at(point!(0, 0)).arc_by(1.m(), -45.deg()); 284 | assert_dir_eq(path.end_direction(), dir!(-1, 1)) 285 | } 286 | 287 | #[test] 288 | fn end_direction_arc_negative_radius_positive_angle() { 289 | let path = Path::at(point!(0, 0)).arc_by(-1.m(), 45.deg()); 290 | assert_dir_eq(path.end_direction(), dir!(1, -1)) 291 | } 292 | 293 | #[test] 294 | fn end_direction_arc_negative_radius_angle() { 295 | let path = Path::at(point!(0, 0)).arc_by(-1.m(), -45.deg()); 296 | assert_dir_eq(path.end_direction(), dir!(-1, -1)) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/core/plane.rs: -------------------------------------------------------------------------------- 1 | use crate::{Axis, Dir, Error, Point, dir, point}; 2 | 3 | /// A 2D plane in 3D space. 4 | #[derive(Debug, PartialEq, Clone, Copy)] 5 | pub struct Plane(Point<3>, Dir<3>, Dir<3>); 6 | impl Plane { 7 | /// Construct the `Plane` spaned by the x and y axes. 8 | pub fn xy() -> Self { 9 | Self(point!(0, 0, 0), dir!(1, 0, 0), dir!(0, 1, 0)) 10 | } 11 | /// Construct the `Plane` spaned by the x and z axes. 12 | pub fn xz() -> Self { 13 | Self(point!(0, 0, 0), dir!(1, 0, 0), dir!(0, 0, 1)) 14 | } 15 | /// Construct the `Plane` spaned by the y and z axes. 16 | pub fn yz() -> Self { 17 | Self(point!(0, 0, 0), dir!(0, 1, 0), dir!(0, 0, 1)) 18 | } 19 | 20 | /// Construct a `Plane` from a point and two orthogonal vectors. 21 | /// 22 | /// `x_axis` defines the direction of the x-axis inside the plane. `y_axis` defines the 23 | /// direction of the y-axis inside the plane. Both are used to project from the local 2D 24 | /// coordinate system to the global coordinate system. If the two axes are not orthogonal, 25 | /// an `Err(Error::VectorsNotOrthogonal)` is returned. 26 | pub fn new(origin: Point<3>, x_dir: Dir<3>, y_dir: Dir<3>) -> Result { 27 | let axes_are_orthogonal = x_dir.dot(y_dir) < 1e-9; 28 | if !axes_are_orthogonal { 29 | return Err(Error::VectorsNotOrthogonal(x_dir, y_dir)); 30 | } 31 | Ok(Self(origin, x_dir, y_dir)) 32 | } 33 | 34 | /// Return the origin point of this `Plane`. 35 | pub fn origin(&self) -> Point<3> { 36 | self.0 37 | } 38 | /// Return the direction of the x-axis of this `Plane`. 39 | pub fn x(&self) -> Dir<3> { 40 | self.1 41 | } 42 | /// Return the x-axis of this `Plane`. 43 | pub fn x_axis(&self) -> Axis<3> { 44 | (self.origin(), self.x()).into() 45 | } 46 | /// Return the direction of the y-axis of this `Plane`. 47 | pub fn y(&self) -> Dir<3> { 48 | self.2 49 | } 50 | /// Return the y-axis of this `Plane`. 51 | pub fn y_axis(&self) -> Axis<3> { 52 | (self.origin(), self.y()).into() 53 | } 54 | /// Return the `Dir3D` that is orthogonal to this plane. 55 | pub fn normal(&self) -> Dir<3> { 56 | self.x().cross(self.y()) 57 | } 58 | /// Return the `Axis::<3>` that is orthogonal to this plane and crosses its origin. 59 | pub fn normal_axis(&self) -> Axis<3> { 60 | (self.origin(), self.normal()).into() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/core/point.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, Div, Mul, Sub}; 2 | 3 | use cxx::UniquePtr; 4 | use iter_fixed::IntoIteratorFixed; 5 | use opencascade_sys::ffi; 6 | 7 | use crate::{Dir, Error, Length, Plane}; 8 | 9 | /// A location in space. 10 | /// 11 | /// `Point`s can be two- or three-dimensional. 12 | /// ```rust 13 | /// use anvil::{IntoLength, Point}; 14 | /// 15 | /// let two_dimensional_point = Point::<2>::new([1.m(), 2.m()]); 16 | /// let three_dimensional_point = Point::<3>::new([1.m(), 2.m(), 3.m()]); 17 | /// ``` 18 | /// 19 | /// The `point!` macro can be used to simplify point construction. 20 | /// ```rust 21 | /// use anvil::{IntoLength, Point, point}; 22 | /// 23 | /// assert_eq!( 24 | /// point!(1.m(), 2.m()), 25 | /// Point::<2>::new([1.m(), 2.m()]) 26 | /// ); 27 | /// assert_eq!( 28 | /// point!(1.m(), 2.m(), 3.m()), 29 | /// Point::<3>::new([1.m(), 2.m(), 3.m()]) 30 | /// ); 31 | /// ``` 32 | #[derive(Debug, PartialEq, Copy, Clone, PartialOrd)] 33 | pub struct Point([Length; DIM]); 34 | impl Point { 35 | /// Construct a `Point` from its coordinate `Length`s. 36 | /// 37 | /// ```rust 38 | /// use anvil::{IntoLength, Point}; 39 | /// 40 | /// // for 2D 41 | /// let point2d = Point::<2>::new([1.m(), 2.m()]); 42 | /// assert_eq!(point2d.x(), 1.m()); 43 | /// assert_eq!(point2d.y(), 2.m()); 44 | /// 45 | /// // for 3D 46 | /// let point3d = Point::<3>::new([1.m(), 2.m(), 3.m()]); 47 | /// assert_eq!(point3d.x(), 1.m()); 48 | /// assert_eq!(point3d.y(), 2.m()); 49 | /// assert_eq!(point3d.z(), 3.m()); 50 | /// ``` 51 | pub fn new(coordinates: [Length; DIM]) -> Self { 52 | Self(coordinates) 53 | } 54 | 55 | /// The origin point with all coordinates equal to zero. 56 | /// 57 | /// ```rust 58 | /// use anvil::{IntoLength, Point}; 59 | /// 60 | /// // for 2D 61 | /// let point2d = Point::<2>::origin(); 62 | /// assert_eq!(point2d.x(), 0.m()); 63 | /// assert_eq!(point2d.y(), 0.m()); 64 | /// 65 | /// // for 3D 66 | /// let point3d = Point::<3>::origin(); 67 | /// assert_eq!(point3d.x(), 0.m()); 68 | /// assert_eq!(point3d.y(), 0.m()); 69 | /// assert_eq!(point3d.z(), 0.m()); 70 | /// ``` 71 | pub fn origin() -> Self { 72 | Self([Length::zero(); DIM]) 73 | } 74 | 75 | /// Return the absolute distance between this `Point` to another. 76 | /// 77 | /// ```rust 78 | /// use core::f64; 79 | /// use anvil::{IntoLength, point}; 80 | /// 81 | /// // for 2D 82 | /// let point2 = point!(3.m(), 4.m()); 83 | /// assert_eq!(point2.distance_to(point!(0, 0)), 5.m()); 84 | /// 85 | /// // for 3D 86 | /// let point3 = point!(3.m(), 0.m(), 4.m()); 87 | /// assert_eq!(point3.distance_to(point!(0, 0, 0)), 5.m()); 88 | /// ``` 89 | pub fn distance_to(&self, other: Self) -> Length { 90 | Length::from_m(f64::sqrt( 91 | (*self - other).0.iter().map(|n| n.m().powi(2)).sum(), 92 | )) 93 | } 94 | 95 | /// Return the direction this `Point` lies in with respect to another point. 96 | pub fn direction_from(&self, other: Self) -> Result, Error> { 97 | Dir::::try_from((*self - other).0.into_iter_fixed().map(|n| n.m()).collect()) 98 | } 99 | } 100 | 101 | impl Point<2> { 102 | /// Return the distance of the `Point<2>` to the origin on the x-axis. 103 | pub fn x(&self) -> Length { 104 | self.0[0] 105 | } 106 | /// Return the distance of the `Point<2>` to the origin on the y-axis. 107 | pub fn y(&self) -> Length { 108 | self.0[1] 109 | } 110 | 111 | /// Return the global position of this `Point<2>` given the `Plane` it is located on. 112 | pub fn to_3d(&self, plane: Plane) -> Point<3> { 113 | plane.origin() + plane.x() * self.x() + plane.y() * self.y() 114 | } 115 | } 116 | 117 | impl Point<3> { 118 | /// Return the distance of the `Point<3>` to the origin on the x-axis. 119 | pub fn x(&self) -> Length { 120 | self.0[0] 121 | } 122 | /// Return the distance of the `Point<3>` to the origin on the y-axis. 123 | pub fn y(&self) -> Length { 124 | self.0[1] 125 | } 126 | /// Return the distance of the `Point<3>` to the origin on the z-axis. 127 | pub fn z(&self) -> Length { 128 | self.0[2] 129 | } 130 | 131 | pub(crate) fn to_occt_point(self) -> UniquePtr { 132 | ffi::new_point(self.x().m(), self.y().m(), self.z().m()) 133 | } 134 | pub(crate) fn to_occt_vec(self) -> UniquePtr { 135 | ffi::new_vec(self.x().m(), self.y().m(), self.z().m()) 136 | } 137 | } 138 | 139 | impl Add for Point { 140 | type Output = Self; 141 | /// Add another `Point` of the same dimension to this one. 142 | /// 143 | /// ```rust 144 | /// use anvil::{IntoLength, point}; 145 | /// 146 | /// // for 2d 147 | /// assert_eq!( 148 | /// point!(1.m(), 2.m()) + point!(4.m(), 5.m()), 149 | /// point!(5.m(), 7.m()) 150 | /// ); 151 | /// 152 | /// // for 3d 153 | /// assert_eq!( 154 | /// point!(1.m(), 2.m(), 3.m()) + point!(4.m(), 5.m(), 6.m()), 155 | /// point!(5.m(), 7.m(), 9.m()) 156 | /// ); 157 | /// ``` 158 | fn add(self, other: Self) -> Self { 159 | Self( 160 | self.0 161 | .into_iter_fixed() 162 | .zip(other.0) 163 | .map(|(a, b)| a + b) 164 | .collect(), 165 | ) 166 | } 167 | } 168 | 169 | impl Sub for Point { 170 | type Output = Self; 171 | /// Subtract another `Point` of the same dimension from this one. 172 | /// 173 | /// ```rust 174 | /// use anvil::{IntoLength, point}; 175 | /// 176 | /// // for 2d 177 | /// assert_eq!( 178 | /// point!(4.m(), 5.m()) - point!(1.m(), 2.m()), 179 | /// point!(3.m(), 3.m()) 180 | /// ); 181 | /// 182 | /// // for 3d 183 | /// assert_eq!( 184 | /// point!(4.m(), 5.m(), 6.m()) - point!(1.m(), 2.m(), 3.m()), 185 | /// point!(3.m(), 3.m(), 3.m()) 186 | /// ); 187 | /// ``` 188 | fn sub(self, other: Self) -> Self { 189 | Self( 190 | self.0 191 | .into_iter_fixed() 192 | .zip(other.0) 193 | .map(|(a, b)| a - b) 194 | .collect(), 195 | ) 196 | } 197 | } 198 | 199 | impl Mul for Point { 200 | type Output = Self; 201 | /// Multiply this `Point` with a scalar. 202 | /// 203 | /// ```rust 204 | /// use anvil::{IntoLength, point}; 205 | /// 206 | /// // for 2d 207 | /// assert_eq!( 208 | /// point!(1.m(), 2.m()) * 3., 209 | /// point!(3.m(), 6.m()) 210 | /// ); 211 | /// 212 | /// // for 3d 213 | /// assert_eq!( 214 | /// point!(1.m(), 2.m(), 3.m()) * 4., 215 | /// point!(4.m(), 8.m(), 12.m()) 216 | /// ); 217 | /// ``` 218 | fn mul(self, other: f64) -> Self { 219 | Self(self.0.into_iter_fixed().map(|n| n * other).collect()) 220 | } 221 | } 222 | 223 | impl Mul> for f64 { 224 | type Output = Point; 225 | /// Multiply this scalar with a `Point`. 226 | /// 227 | /// ```rust 228 | /// use anvil::{IntoLength, point}; 229 | /// 230 | /// // for 2d 231 | /// assert_eq!( 232 | /// 3. * point!(1.m(), 2.m()), 233 | /// point!(3.m(), 6.m()) 234 | /// ); 235 | /// 236 | /// // for 3d 237 | /// assert_eq!( 238 | /// 4. * point!(1.m(), 2.m(), 3.m()), 239 | /// point!(4.m(), 8.m(), 12.m()) 240 | /// ); 241 | /// ``` 242 | fn mul(self, other: Point) -> Point { 243 | other * self 244 | } 245 | } 246 | 247 | impl Div for Point { 248 | type Output = Self; 249 | /// Divide this `Point` by a scalar. 250 | /// 251 | /// ```rust 252 | /// use anvil::{IntoLength, point}; 253 | /// 254 | /// // for 2d 255 | /// assert_eq!( 256 | /// point!(3.m(), 6.m()) / 3., 257 | /// point!(1.m(), 2.m()) 258 | /// ); 259 | /// 260 | /// // for 3d 261 | /// assert_eq!( 262 | /// point!(4.m(), 8.m(), 12.m()) / 4., 263 | /// point!(1.m(), 2.m(), 3.m()) 264 | /// ); 265 | /// ``` 266 | fn div(self, other: f64) -> Self { 267 | Self(self.0.into_iter_fixed().map(|n| n / other).collect()) 268 | } 269 | } 270 | 271 | /// Macro for simplifying `Point` construction for static values. 272 | /// 273 | /// # Examples 274 | /// ```rust 275 | /// use anvil::{IntoLength, point, Point}; 276 | /// 277 | /// // construct a Point<2> from two `Length` values 278 | /// assert_eq!( 279 | /// point!(3.m(), 4.cm()), 280 | /// Point::<2>::new([3.m(), 4.cm()]) 281 | /// ); 282 | /// assert_eq!(point!(0, 0), Point::<2>::origin()); 283 | /// 284 | /// // construct a Point<3> from three `Length` values 285 | /// assert_eq!( 286 | /// point!(3.m(), 4.cm(), 5.yd()), 287 | /// Point::<3>::new([3.m(), 4.cm(), 5.yd()]) 288 | /// ); 289 | /// assert_eq!(point!(0, 0, 0), Point::<3>::origin()); 290 | /// ``` 291 | #[macro_export] 292 | macro_rules! point { 293 | (0, 0) => { 294 | $crate::Point::<2>::origin() 295 | }; 296 | ($x:expr, $y:expr) => { 297 | $crate::Point::<2>::new([$x, $y]) 298 | }; 299 | 300 | (0, 0, 0) => { 301 | $crate::Point::<3>::origin() 302 | }; 303 | ($x:expr, $y:expr, $z:expr) => { 304 | $crate::Point::<3>::new([$x, $y, $z]) 305 | }; 306 | } 307 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error as StdError, fmt, path::PathBuf}; 2 | 3 | use crate::Dir; 4 | 5 | /// The errors that can occurr. 6 | #[derive(Clone, Debug, PartialEq)] 7 | pub enum Error { 8 | /// Occurs when a function that requires a non-empty `Part` is called on an empty one. 9 | EmptyPart, 10 | 11 | /// Occurs when a function that requires a non-empty `Sketch` is called on an empty one. 12 | EmptySketch, 13 | 14 | /// Occurs when a `Part` could not be written to a .step file at a given path. 15 | StepWrite(PathBuf), 16 | 17 | /// Occurs when a `Part` could not be written to a .stl file at a given path. 18 | StlWrite(PathBuf), 19 | 20 | /// Occurs when a `Face` or `Part` can not be triangulated. 21 | Triangulation, 22 | 23 | /// Occurs when two vectors that are required to be orthogonal, are not. 24 | VectorsNotOrthogonal(Dir<3>, Dir<3>), 25 | 26 | /// Occurs when an operation that requires a length is performed on a `Dir3D` with a magnitude of zero. 27 | ZeroVector, 28 | } 29 | impl StdError for Error {} 30 | impl fmt::Display for Error { 31 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 32 | write!(f, "Under construction") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/faces/face.rs: -------------------------------------------------------------------------------- 1 | use cxx::UniquePtr; 2 | use opencascade_sys::ffi; 3 | 4 | /// A 2D surface that has a clear bound. 5 | pub struct Face(pub(crate) UniquePtr); 6 | impl Face { 7 | pub(crate) fn from_occt(occt: &ffi::TopoDS_Face) -> Self { 8 | Self(ffi::TopoDS_Face_to_owned(occt)) 9 | } 10 | } 11 | 12 | impl Clone for Face { 13 | fn clone(&self) -> Self { 14 | Self::from_occt(&self.0) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/faces/iterator.rs: -------------------------------------------------------------------------------- 1 | use cxx::UniquePtr; 2 | use opencascade_sys::ffi; 3 | 4 | use crate::Part; 5 | 6 | use super::face::Face; 7 | 8 | /// Iterator over the `Face`s of a `Part`. 9 | /// 10 | /// ```rust 11 | /// use anvil::{Cube, Face, FaceIterator, IntoLength}; 12 | /// 13 | /// let face_iterator: FaceIterator = Cube::from_size(1.m()).faces(); 14 | /// for face in face_iterator { 15 | /// // ... 16 | /// } 17 | /// ``` 18 | pub enum FaceIterator { 19 | /// A FaceIterator that is not empty. 20 | NotEmpty(Part, UniquePtr), 21 | /// A FaceIterator from an empty shape. 22 | Empty, 23 | } 24 | 25 | impl Iterator for FaceIterator { 26 | type Item = Face; 27 | 28 | fn next(&mut self) -> Option { 29 | match self { 30 | Self::NotEmpty(_, explorer) => { 31 | if explorer.More() { 32 | let face = ffi::TopoDS_cast_to_face(explorer.Current()); 33 | let face = Face::from_occt(face); 34 | explorer.pin_mut().Next(); 35 | Some(face) 36 | } else { 37 | None 38 | } 39 | } 40 | Self::Empty => None, 41 | } 42 | } 43 | } 44 | impl ExactSizeIterator for FaceIterator { 45 | fn len(&self) -> usize { 46 | match self { 47 | Self::NotEmpty(_, _) => { 48 | let mut len = 0; 49 | for _ in self.clone_without_position() { 50 | len += 1; 51 | } 52 | len 53 | } 54 | Self::Empty => 0, 55 | } 56 | } 57 | } 58 | impl FaceIterator { 59 | /// Return `true` if this `FaceIterator` has a length of 0. 60 | pub fn is_empty(self) -> bool { 61 | self.len() == 0 62 | } 63 | fn clone_without_position(&self) -> Self { 64 | match self { 65 | Self::NotEmpty(part, _) => part.faces(), 66 | Self::Empty => Self::Empty, 67 | } 68 | } 69 | } 70 | impl From<&Part> for FaceIterator { 71 | fn from(value: &Part) -> Self { 72 | match &value.inner { 73 | Some(inner) => { 74 | let explorer = ffi::TopExp_Explorer_ctor(inner, ffi::TopAbs_ShapeEnum::TopAbs_FACE); 75 | Self::NotEmpty(value.clone(), explorer) 76 | } 77 | None => Self::Empty, 78 | } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | 86 | #[test] 87 | fn empty() { 88 | assert!(Part::empty().faces().is_empty()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/faces/mod.rs: -------------------------------------------------------------------------------- 1 | mod face; 2 | mod iterator; 3 | 4 | pub use face::Face; 5 | pub use iterator::FaceIterator; 6 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = "A CAD engine."] 2 | #![allow(clippy::approx_constant)] 3 | #![warn(missing_docs)] 4 | #![warn(clippy::todo)] 5 | #![warn(clippy::unimplemented)] 6 | 7 | mod core; 8 | mod errors; 9 | mod faces; 10 | mod meshes; 11 | mod parts; 12 | mod sketches; 13 | 14 | pub use core::{ 15 | Angle, Axis, Dir, Edge, IntoAngle, IntoF64, IntoLength, Length, Path, Plane, Point, 16 | }; 17 | pub use errors::Error; 18 | pub use faces::{Face, FaceIterator}; 19 | pub use meshes::RenderMesh; 20 | pub use parts::{ 21 | Part, 22 | primitives::{Cube, Cuboid, Cylinder, Sphere}, 23 | }; 24 | pub use sketches::{ 25 | Sketch, 26 | primitives::{Circle, Rectangle, Square}, 27 | }; 28 | -------------------------------------------------------------------------------- /src/meshes/mod.rs: -------------------------------------------------------------------------------- 1 | mod render_mesh; 2 | 3 | pub use render_mesh::RenderMesh; 4 | -------------------------------------------------------------------------------- /src/meshes/render_mesh.rs: -------------------------------------------------------------------------------- 1 | use opencascade_sys::ffi; 2 | 3 | use crate::{Dir, Error, Face, IntoLength, Length, Part, Point}; 4 | 5 | const DEFAULT_TOLERANCE: Length = Length::from_m(0.000001); 6 | 7 | /// A triangular mesh of one or more `Face`s optimized for 3D rendering. 8 | #[derive(Clone, Debug, PartialEq)] 9 | pub struct RenderMesh { 10 | points: Vec>, 11 | indices: Vec<[usize; 3]>, 12 | normals: Vec>, 13 | uvs: Vec<[f64; 2]>, 14 | } 15 | impl RenderMesh { 16 | /// Return a clone of this `RenderMesh` with the individual indices sorted. 17 | /// 18 | /// Sorting of the triangle indices depends on the machine executing the tests which introduces 19 | /// non-deterministic behavior. This function enables comparing `RenderMesh`es across devices. 20 | pub fn sorted(&self) -> Self { 21 | Self { 22 | points: self.points.clone(), 23 | indices: { 24 | let mut sorted_indices = vec![]; 25 | for triangle in self.indices.clone() { 26 | let mut sorted_triangle = triangle; 27 | sorted_triangle.sort(); 28 | sorted_indices.push(sorted_triangle); 29 | } 30 | sorted_indices 31 | }, 32 | normals: self.normals.clone(), 33 | uvs: self.uvs.clone(), 34 | } 35 | } 36 | 37 | /// Return the `Point`s of this `RenderMesh`. 38 | pub fn points(&self) -> &Vec> { 39 | &self.points 40 | } 41 | /// Return the `Point` indices defining the triangles of this `RenderMesh`. 42 | pub fn indices(&self) -> &Vec<[usize; 3]> { 43 | &self.indices 44 | } 45 | /// Return the normal `Dir` of every `Point` in this `RenderMesh`. 46 | pub fn normals(&self) -> &Vec> { 47 | &self.normals 48 | } 49 | /// Return the relative position of every `Point` on the 2D-grid of this `RenderMesh`. 50 | pub fn uvs(&self) -> &Vec<[f64; 2]> { 51 | &self.uvs 52 | } 53 | 54 | /// Return the collective area spanned by the triangles in a `RenderedMesh` in square meters. 55 | /// 56 | /// ```rust 57 | /// use anvil::{Cube, IntoLength, Plane, Rectangle, RenderMesh}; 58 | /// 59 | /// let rect = Rectangle::from_dim(2.m(), 3.m()); 60 | /// let mesh = RenderMesh::try_from(rect.to_face(Plane::xy()).unwrap()).unwrap(); 61 | /// assert!((mesh.area() - 6.).abs() < 0.0001); 62 | /// 63 | /// let cube = Cube::from_size(2.m()); 64 | /// let mesh = RenderMesh::try_from(cube).unwrap(); 65 | /// assert!((mesh.area() - 24.).abs() < 0.0001); 66 | /// ``` 67 | pub fn area(&self) -> f64 { 68 | let mut total_area = 0.; 69 | for triangle in &self.indices { 70 | let point1 = *self 71 | .points 72 | .get(triangle[0]) 73 | .expect("index should be a valid point"); 74 | let point2 = *self 75 | .points 76 | .get(triangle[1]) 77 | .expect("index should be a valid point"); 78 | let point3 = *self 79 | .points 80 | .get(triangle[2]) 81 | .expect("index should be a valid point"); 82 | let edge_1 = point2 - point1; 83 | let edge_2 = point3 - point1; 84 | 85 | let cross = ( 86 | edge_1.y().m() * edge_2.z().m() - edge_1.z().m() * edge_2.y().m(), 87 | edge_1.z().m() * edge_2.x().m() - edge_1.x().m() * edge_2.z().m(), 88 | edge_1.x().m() * edge_2.y().m() - edge_1.y().m() * edge_2.x().m(), 89 | ); 90 | 91 | total_area += 0.5 * f64::sqrt(cross.0.powi(2) + cross.1.powi(2) + cross.2.powi(2)); 92 | } 93 | total_area 94 | } 95 | /// Return the center point of the `RenderMesh`, i.e. the average of all mesh points. 96 | /// 97 | /// ```rust 98 | /// use anvil::{IntoLength, Plane, Rectangle, RenderMesh, point}; 99 | /// 100 | /// let rect = Rectangle::from_dim(1.m(), 1.m()).move_to(point!(2.m(), 3.m())); 101 | /// let mesh = RenderMesh::try_from(rect.to_face(Plane::xy()).unwrap()).unwrap(); 102 | /// let mesh_center = mesh.center(); 103 | /// assert!((mesh_center.x() - 2.m()).abs() < 0.0001.m()); 104 | /// assert!((mesh_center.y() - 3.m()).abs() < 0.0001.m()); 105 | /// assert!(mesh_center.z().abs() < 0.0001.m()); 106 | /// ``` 107 | pub fn center(&self) -> Point<3> { 108 | let mut sum_of_points = Point::<3>::origin(); 109 | for point in &self.points { 110 | sum_of_points = sum_of_points + *point; 111 | } 112 | sum_of_points / self.points.len() as f64 113 | } 114 | 115 | fn empty() -> Self { 116 | Self { 117 | points: vec![], 118 | indices: vec![], 119 | normals: vec![], 120 | uvs: vec![], 121 | } 122 | } 123 | 124 | fn merge_with(&mut self, other: Self) { 125 | self.indices.extend(other.indices().iter().map(|t| { 126 | [ 127 | t[0] + self.points.len(), 128 | t[1] + self.points.len(), 129 | t[2] + self.points.len(), 130 | ] 131 | })); 132 | self.points.extend(other.points()); 133 | self.normals.extend(other.normals()); 134 | self.uvs.extend(other.uvs()); 135 | } 136 | } 137 | 138 | impl TryFrom for RenderMesh { 139 | type Error = Error; 140 | fn try_from(face: Face) -> Result { 141 | (face, DEFAULT_TOLERANCE).try_into() 142 | } 143 | } 144 | impl TryFrom<(Face, Length)> for RenderMesh { 145 | type Error = Error; 146 | fn try_from((face, tolerance): (Face, Length)) -> Result { 147 | let mesh = ffi::BRepMesh_IncrementalMesh_ctor( 148 | ffi::cast_face_to_shape(face.0.as_ref().unwrap()), 149 | tolerance.m(), 150 | ); 151 | let face = ffi::TopoDS_cast_to_face(mesh.as_ref().unwrap().Shape()); 152 | let mut location = ffi::TopLoc_Location_ctor(); 153 | 154 | let triangulation_handle = ffi::BRep_Tool_Triangulation(face, location.pin_mut()); 155 | let transformation = ffi::TopLoc_Location_Transformation(&location); 156 | 157 | if let Ok(triangulation) = ffi::HandlePoly_Triangulation_Get(&triangulation_handle) { 158 | let mut points = vec![]; 159 | let mut indices = vec![]; 160 | let mut normals = vec![]; 161 | let mut uvs = vec![]; 162 | 163 | let orientation = face.Orientation(); 164 | let face_point_count = triangulation.NbNodes(); 165 | ffi::compute_normals(face, &triangulation_handle); 166 | 167 | for node_index in 1..=face_point_count { 168 | let mut point = ffi::Poly_Triangulation_Node(triangulation, node_index); 169 | point.pin_mut().Transform(&transformation); 170 | points.push(Point::<3>::new([ 171 | point.X().m(), 172 | point.Y().m(), 173 | point.Z().m(), 174 | ])); 175 | 176 | let uv = ffi::Poly_Triangulation_UV(triangulation, node_index); 177 | uvs.push([uv.X(), uv.Y()]); 178 | 179 | let mut normal = ffi::Poly_Triangulation_Normal(triangulation, node_index); 180 | normal.pin_mut().Transform(&transformation); 181 | let m = if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { 182 | -1. 183 | } else { 184 | 1. 185 | }; 186 | normals.push( 187 | Dir::try_from([normal.X() * m, normal.Y() * m, normal.Z() * m]) 188 | .expect("normals should not be zero"), 189 | ); 190 | } 191 | 192 | let mut u_min = f64::INFINITY; 193 | let mut v_min = f64::INFINITY; 194 | let mut u_max = f64::NEG_INFINITY; 195 | let mut v_max = f64::NEG_INFINITY; 196 | 197 | for &[u, v] in &uvs { 198 | u_min = u_min.min(u); 199 | v_min = v_min.min(v); 200 | u_max = u_max.max(u); 201 | v_max = v_max.max(v); 202 | } 203 | 204 | for [u, v] in &mut uvs { 205 | *u = (*u - u_min) / (u_max - u_min); 206 | *v = (*v - v_min) / (v_max - v_min); 207 | 208 | if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { 209 | *u = 1.0 - *u; 210 | } 211 | } 212 | 213 | for triangle_index in 1..=triangulation.NbTriangles() { 214 | let triangle = triangulation.Triangle(triangle_index); 215 | let mut node_ids = [triangle.Value(1), triangle.Value(2), triangle.Value(3)] 216 | .map(|id| id as usize - 1); 217 | 218 | if orientation == ffi::TopAbs_Orientation::TopAbs_REVERSED { 219 | node_ids.swap(1, 2); 220 | } 221 | indices.push(node_ids); 222 | } 223 | 224 | Ok(RenderMesh { 225 | points, 226 | indices, 227 | normals, 228 | uvs, 229 | }) 230 | } else { 231 | Err(Error::Triangulation) 232 | } 233 | } 234 | } 235 | impl TryFrom for RenderMesh { 236 | type Error = Error; 237 | fn try_from(part: Part) -> Result { 238 | (part, DEFAULT_TOLERANCE).try_into() 239 | } 240 | } 241 | impl TryFrom<(Part, Length)> for RenderMesh { 242 | type Error = Error; 243 | fn try_from((part, tolerance): (Part, Length)) -> Result { 244 | let meshes = part 245 | .faces() 246 | .map(|face| RenderMesh::try_from((face, tolerance))) 247 | .collect::, Error>>()?; 248 | Ok(merge(meshes)) 249 | } 250 | } 251 | 252 | fn merge(meshes: Vec) -> RenderMesh { 253 | let mut merged_mesh = RenderMesh::empty(); 254 | for mesh in meshes { 255 | merged_mesh.merge_with(mesh); 256 | } 257 | merged_mesh 258 | } 259 | 260 | #[cfg(test)] 261 | mod tests { 262 | use core::f64; 263 | 264 | use crate::{Axis, Circle, Cube, IntoAngle, IntoLength, Path, Plane, Rectangle, dir, point}; 265 | 266 | use super::*; 267 | 268 | #[test] 269 | fn triangle() { 270 | let face = Path::at(point!(0, 0)) 271 | .line_to(point!(1.m(), 0.m())) 272 | .line_to(point!(0.m(), 1.m())) 273 | .close() 274 | .to_face(Plane::xy()) 275 | .unwrap(); 276 | 277 | assert_eq!( 278 | RenderMesh::try_from(face).unwrap().sorted(), 279 | RenderMesh { 280 | points: vec![ 281 | point!(0, 0, 0), 282 | point!(1.m(), 0.m(), 0.m()), 283 | point!(0.m(), 1.m(), 0.m()) 284 | ], 285 | indices: vec![[0, 1, 2]], 286 | normals: vec![dir!(0, 0, 1), dir!(0, 0, 1), dir!(0, 0, 1)], 287 | uvs: vec![[0., 0.], [1., 0.], [0., 1.]] 288 | } 289 | ) 290 | } 291 | 292 | #[test] 293 | fn rectangle() { 294 | let face = Rectangle::from_corners(point!(0, 0), point!(1.m(), 1.m())) 295 | .to_face(Plane::xy()) 296 | .unwrap(); 297 | 298 | assert_eq!( 299 | RenderMesh::try_from(face).unwrap().sorted(), 300 | RenderMesh { 301 | points: vec![ 302 | point!(0, 0, 0), 303 | point!(1.m(), 0.m(), 0.m()), 304 | point!(1.m(), 1.m(), 0.m()), 305 | point!(0.m(), 1.m(), 0.m()), 306 | ], 307 | indices: vec![[0, 1, 2], [0, 2, 3]], 308 | normals: vec![dir!(0, 0, 1), dir!(0, 0, 1), dir!(0, 0, 1), dir!(0, 0, 1)], 309 | uvs: vec![[0., 0.], [1., 0.], [1., 1.], [0., 1.]] 310 | } 311 | ) 312 | } 313 | 314 | #[test] 315 | fn circle() { 316 | let mesh = 317 | RenderMesh::try_from(Circle::from_radius(1.m()).to_face(Plane::xy()).unwrap()).unwrap(); 318 | assert!(mesh.center().x().abs().m() < 0.00001); 319 | assert!(mesh.center().y().abs().m() < 0.00001); 320 | assert!(mesh.center().z().abs().m() < 0.00001); 321 | assert!((mesh.area() - f64::consts::PI).abs() < 0.00001); 322 | 323 | assert_eq!(mesh.normals(), &vec![dir!(0, 0, -1); mesh.normals().len()]); 324 | } 325 | 326 | #[test] 327 | fn rotated_cube_has_correct_normals() { 328 | let cube = Cube::from_size(1.m()) 329 | .rotate_around(Axis::<3>::x(), 45.deg()) 330 | .rotate_around(Axis::<3>::z(), 45.deg()); 331 | let mesh = 332 | RenderMesh::try_from(cube.faces().collect::>().first().unwrap().clone()) 333 | .unwrap(); 334 | for normal in mesh.normals { 335 | assert!(normal.approx_eq(dir!(-1, -1, 0))) 336 | } 337 | } 338 | 339 | #[test] 340 | fn cube() { 341 | let cube_mesh = RenderMesh::try_from(Cube::from_size(2.m())) 342 | .unwrap() 343 | .sorted(); 344 | assert_eq!( 345 | cube_mesh.points(), 346 | &vec![ 347 | // -x face 348 | point!(-1.m(), -1.m(), -1.m()), 349 | point!(-1.m(), -1.m(), 1.m()), 350 | point!(-1.m(), 1.m(), -1.m()), 351 | point!(-1.m(), 1.m(), 1.m()), 352 | // +x face 353 | point!(1.m(), -1.m(), -1.m()), 354 | point!(1.m(), -1.m(), 1.m()), 355 | point!(1.m(), 1.m(), -1.m()), 356 | point!(1.m(), 1.m(), 1.m()), 357 | // -y face 358 | point!(-1.m(), -1.m(), -1.m()), 359 | point!(1.m(), -1.m(), -1.m()), 360 | point!(-1.m(), -1.m(), 1.m()), 361 | point!(1.m(), -1.m(), 1.m()), 362 | // +y face 363 | point!(-1.m(), 1.m(), -1.m()), 364 | point!(1.m(), 1.m(), -1.m()), 365 | point!(-1.m(), 1.m(), 1.m()), 366 | point!(1.m(), 1.m(), 1.m()), 367 | // -z face 368 | point!(-1.m(), -1.m(), -1.m()), 369 | point!(-1.m(), 1.m(), -1.m()), 370 | point!(1.m(), -1.m(), -1.m()), 371 | point!(1.m(), 1.m(), -1.m()), 372 | // +z face 373 | point!(-1.m(), -1.m(), 1.m()), 374 | point!(-1.m(), 1.m(), 1.m()), 375 | point!(1.m(), -1.m(), 1.m()), 376 | point!(1.m(), 1.m(), 1.m()), 377 | ] 378 | ); 379 | assert_eq!( 380 | cube_mesh.indices(), 381 | &vec![ 382 | // -x face 383 | [0, 1, 2], 384 | [1, 2, 3], 385 | // +x face 386 | [4, 5, 6], 387 | [5, 6, 7], 388 | // -y face 389 | [8, 9, 11], 390 | [8, 10, 11], 391 | // +y face 392 | [12, 13, 15], 393 | [12, 14, 15], 394 | // -z face 395 | [16, 17, 19], 396 | [16, 18, 19], 397 | // +z face 398 | [20, 21, 23], 399 | [20, 22, 23], 400 | ] 401 | ); 402 | assert_eq!( 403 | cube_mesh.normals(), 404 | &vec![ 405 | // -x face 406 | dir!(-1, 0, 0), 407 | dir!(-1, 0, 0), 408 | dir!(-1, 0, 0), 409 | dir!(-1, 0, 0), 410 | // +x face 411 | dir!(1, 0, 0), 412 | dir!(1, 0, 0), 413 | dir!(1, 0, 0), 414 | dir!(1, 0, 0), 415 | // -y face 416 | dir!(0, -1, 0), 417 | dir!(0, -1, 0), 418 | dir!(0, -1, 0), 419 | dir!(0, -1, 0), 420 | // +y face 421 | dir!(0, 1, 0), 422 | dir!(0, 1, 0), 423 | dir!(0, 1, 0), 424 | dir!(0, 1, 0), 425 | // -z face 426 | dir!(0, 0, -1), 427 | dir!(0, 0, -1), 428 | dir!(0, 0, -1), 429 | dir!(0, 0, -1), 430 | // +z face 431 | dir!(0, 0, 1), 432 | dir!(0, 0, 1), 433 | dir!(0, 0, 1), 434 | dir!(0, 0, 1), 435 | ] 436 | ) 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/parts/mod.rs: -------------------------------------------------------------------------------- 1 | mod part; 2 | pub mod primitives; 3 | 4 | pub use part::Part; 5 | -------------------------------------------------------------------------------- /src/parts/part.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Debug, 3 | fs, 4 | io::{self, BufRead}, 5 | path::Path, 6 | }; 7 | 8 | use cxx::UniquePtr; 9 | use opencascade_sys::ffi; 10 | use tempfile::NamedTempFile; 11 | 12 | use crate::{Angle, Axis, Error, FaceIterator, IntoAngle, Length, Point, point}; 13 | 14 | /// A 3D object in space. 15 | pub struct Part { 16 | pub(crate) inner: Option>, 17 | } 18 | impl Part { 19 | /// Construct an empty `Part` which can be used for merging with other parts. 20 | /// 21 | /// ```rust 22 | /// use anvil::Part; 23 | /// 24 | /// let part = Part::empty(); 25 | /// assert_eq!(part.volume(), 0.); 26 | /// ``` 27 | pub fn empty() -> Self { 28 | Self { inner: None } 29 | } 30 | 31 | /// Return true if this `Part` is empty. 32 | /// 33 | /// ```rust 34 | /// use anvil::{Cube, IntoLength, Part}; 35 | /// 36 | /// let cube = Cube::from_size(1.m()); 37 | /// assert!(!cube.is_empty()); 38 | /// assert!(cube.subtract(&cube).is_empty()); 39 | /// ``` 40 | pub fn is_empty(&self) -> bool { 41 | self.volume() < 1e-9 42 | } 43 | 44 | /// Merge this `Part` with another. 45 | /// 46 | /// ```rust 47 | /// use anvil::{Cuboid, point, IntoLength}; 48 | /// 49 | /// let cuboid1 = Cuboid::from_corners(point!(0, 0, 0), point!(1.m(), 1.m(), 1.m())); 50 | /// let cuboid2 = Cuboid::from_corners(point!(0.m(), 0.m(), 1.m()), point!(1.m(), 1.m(), 2.m())); 51 | /// 52 | /// assert_eq!( 53 | /// cuboid1.add(&cuboid2), 54 | /// Cuboid::from_corners(point!(0, 0, 0), point!(1.m(), 1.m(), 2.m())) 55 | /// ) 56 | /// ``` 57 | pub fn add(&self, other: &Self) -> Self { 58 | match (&self.inner, &other.inner) { 59 | (Some(self_inner), Some(other_inner)) => { 60 | let mut fuse_operation = ffi::BRepAlgoAPI_Fuse_ctor(self_inner, other_inner); 61 | Self::from_occt(fuse_operation.pin_mut().Shape()) 62 | } 63 | (Some(_), None) => self.clone(), 64 | (None, Some(_)) => other.clone(), 65 | (None, None) => self.clone(), 66 | } 67 | } 68 | 69 | /// Create multiple instances of the `Part` spaced evenly around a point. 70 | /// 71 | /// ```rust 72 | /// use anvil::{Axis, Cuboid, IntoAngle, IntoLength, point}; 73 | /// 74 | /// let cuboid = Cuboid::from_corners(point!(1.m(), 1.m(), 0.m()), point!(2.m(), 2.m(), 1.m())); 75 | /// assert_eq!( 76 | /// cuboid.circular_pattern(Axis::<3>::z(), 4), 77 | /// cuboid 78 | /// .add(&cuboid.rotate_around(Axis::<3>::z(), 90.deg())) 79 | /// .add(&cuboid.rotate_around(Axis::<3>::z(), 180.deg())) 80 | /// .add(&cuboid.rotate_around(Axis::<3>::z(), 270.deg())) 81 | /// ) 82 | /// ``` 83 | pub fn circular_pattern(&self, around: Axis<3>, instances: u8) -> Self { 84 | let angle_step = 360.deg() / instances as f64; 85 | let mut new_shape = self.clone(); 86 | let mut angle = 0.rad(); 87 | for _ in 0..instances { 88 | new_shape = new_shape.add(&self.rotate_around(around, angle)); 89 | angle = angle + angle_step; 90 | } 91 | new_shape 92 | } 93 | /// Return the faces spanned by this `Part`. 94 | /// 95 | /// ```rust 96 | /// use anvil::{Cube, Cylinder, IntoLength, Sphere}; 97 | /// 98 | /// assert_eq!(Cube::from_size(1.m()).faces().len(), 6); 99 | /// assert_eq!(Cylinder::from_radius(1.m(), 1.m()).faces().len(), 3); 100 | /// assert_eq!(Sphere::from_radius(1.m()).faces().len(), 1); 101 | /// ``` 102 | pub fn faces(&self) -> FaceIterator { 103 | self.into() 104 | } 105 | /// Return the `Part` that is created from the overlapping volume between this one and another. 106 | /// 107 | /// ```rust 108 | /// use anvil::{Cuboid, IntoLength}; 109 | /// 110 | /// let cuboid1 = Cuboid::from_dim(5.m(), 5.m(), 1.m()); 111 | /// let cuboid2 = Cuboid::from_dim(1.m(), 1.m(), 5.m()); 112 | /// assert_eq!( 113 | /// cuboid1.intersect(&cuboid2), 114 | /// Cuboid::from_dim(1.m(), 1.m(), 1.m()) 115 | /// ) 116 | /// ``` 117 | pub fn intersect(&self, other: &Self) -> Self { 118 | match (&self.inner, &other.inner) { 119 | (Some(self_inner), Some(other_inner)) => { 120 | let mut fuse_operation = ffi::BRepAlgoAPI_Common_ctor(self_inner, other_inner); 121 | Self::from_occt(fuse_operation.pin_mut().Shape()) 122 | } 123 | _ => Part { inner: None }, 124 | } 125 | } 126 | 127 | /// Create multiple instances of the `Part` spaced evenly until a point. 128 | /// 129 | /// ```rust 130 | /// use anvil::{Cuboid, IntoLength, point}; 131 | /// 132 | /// let cuboid = Cuboid::from_dim(1.m(), 1.m(), 1.m()); 133 | /// assert_eq!( 134 | /// cuboid.linear_pattern(point!(4.m(), 0.m(), 0.m()), 5), 135 | /// cuboid 136 | /// .add(&cuboid.move_to(point!(1.m(), 0.m(), 0.m()))) 137 | /// .add(&cuboid.move_to(point!(2.m(), 0.m(), 0.m()))) 138 | /// .add(&cuboid.move_to(point!(3.m(), 0.m(), 0.m()))) 139 | /// .add(&cuboid.move_to(point!(4.m(), 0.m(), 0.m()))) 140 | /// ) 141 | /// ``` 142 | pub fn linear_pattern(&self, until: Point<3>, instances: u8) -> Self { 143 | let start = match self.center() { 144 | Ok(p) => p, 145 | Err(_) => return self.clone(), 146 | }; 147 | let axis = match Axis::<3>::between(start, until) { 148 | Ok(axis) => axis, 149 | Err(_) => return self.clone(), 150 | }; 151 | 152 | let len_step = (start - until).distance_to(Point::<3>::origin()) / instances as f64; 153 | let mut new_part = self.clone(); 154 | let mut pos = Length::zero(); 155 | for _ in 0..instances { 156 | pos = pos + len_step; 157 | new_part = new_part.add(&self.move_to(axis.point_at(pos))); 158 | } 159 | new_part 160 | } 161 | /// Return a clone of this `Part` moved by a specified amount in each axis. 162 | /// 163 | /// ```rust 164 | /// use anvil::{Cuboid, IntoLength, point}; 165 | /// 166 | /// let cuboid = Cuboid::from_dim(1.m(), 1.m(), 1.m()); 167 | /// let moved_cuboid = cuboid 168 | /// .move_by(1.m(), 0.m(), 3.m()) 169 | /// .move_by(0.m(), 1.m(), 0.m()); 170 | /// assert_eq!( 171 | /// moved_cuboid.center(), 172 | /// Ok(point!(1.m(), 1.m(), 3.m())) 173 | /// ) 174 | /// ``` 175 | pub fn move_by(&self, dx: Length, dy: Length, dz: Length) -> Self { 176 | let center = match self.center() { 177 | Ok(c) => c, 178 | Err(_) => return self.clone(), 179 | }; 180 | self.move_to(center + point!(dx, dy, dz)) 181 | } 182 | /// Return a clone of this `Part` with the center moved to a specified point. 183 | /// 184 | /// ```rust 185 | /// use anvil::{Cuboid, IntoLength, point}; 186 | /// 187 | /// let cuboid = Cuboid::from_dim(1.m(), 1.m(), 1.m()); 188 | /// let moved_cuboid = cuboid.move_to(point!(2.m(), 2.m(), 2.m())); 189 | /// assert_eq!(cuboid.center(), Ok(point!(0, 0, 0))); 190 | /// assert_eq!(moved_cuboid.center(), Ok(point!(2.m(), 2.m(), 2.m()))); 191 | /// ``` 192 | pub fn move_to(&self, loc: Point<3>) -> Self { 193 | match &self.inner { 194 | Some(inner) => { 195 | let move_vec = (loc - self.center().unwrap()).to_occt_vec(); 196 | let mut transform = ffi::new_transform(); 197 | transform.pin_mut().set_translation_vec(&move_vec); 198 | let mut operation = ffi::BRepBuilderAPI_Transform_ctor(inner, &transform, false); 199 | Self::from_occt(operation.pin_mut().Shape()) 200 | } 201 | None => Self { inner: None }, 202 | } 203 | } 204 | /// Return a clone of this `Part` rotated around an `Axis::<3>`. 205 | /// 206 | /// For positive angles, the right-hand-rule applies for the direction of rotation. 207 | /// 208 | /// ```rust 209 | /// use anvil::{Axis, Cuboid, IntoAngle, IntoLength, point}; 210 | /// 211 | /// let cuboid = Cuboid::from_corners(point!(0, 0, 0), point!(1.m(), 1.m(), 1.m())); 212 | /// assert_eq!( 213 | /// cuboid.rotate_around(Axis::<3>::x(), 90.deg()), 214 | /// Cuboid::from_corners(point!(0, 0, 0), point!(1.m(), -1.m(), 1.m())) 215 | /// ) 216 | /// ``` 217 | pub fn rotate_around(&self, axis: Axis<3>, angle: Angle) -> Self { 218 | match &self.inner { 219 | Some(inner) => { 220 | let mut transform = ffi::new_transform(); 221 | transform 222 | .pin_mut() 223 | .SetRotation(&axis.to_occt_ax1(), angle.rad()); 224 | let mut operation = ffi::BRepBuilderAPI_Transform_ctor(inner, &transform, false); 225 | Self::from_occt(operation.pin_mut().Shape()) 226 | } 227 | None => Self { inner: None }, 228 | } 229 | } 230 | /// Return a clone of this `Part` with the size scaled by a factor. 231 | /// 232 | /// ```rust 233 | /// use anvil::{Cuboid, IntoLength}; 234 | /// 235 | /// let cuboid = Cuboid::from_dim(1.m(), 1.m(), 1.m()); 236 | /// assert_eq!( 237 | /// cuboid.scale(2.), 238 | /// Cuboid::from_dim(2.m(), 2.m(), 2.m()) 239 | /// ) 240 | /// ``` 241 | pub fn scale(&self, factor: f64) -> Self { 242 | match &self.inner { 243 | Some(inner) => { 244 | let mut transform = ffi::new_transform(); 245 | transform.pin_mut().SetScale( 246 | &self.center().expect("shape is not empty").to_occt_point(), 247 | factor, 248 | ); 249 | let mut operation = ffi::BRepBuilderAPI_Transform_ctor(inner, &transform, false); 250 | Self::from_occt(operation.pin_mut().Shape()) 251 | } 252 | None => Self { inner: None }, 253 | } 254 | } 255 | /// Return a copy of this `Part` with the intersection of another removed. 256 | /// 257 | /// ```rust 258 | /// use anvil::{Cuboid, IntoLength, point}; 259 | /// 260 | /// let cuboid1 = Cuboid::from_corners(point!(0, 0, 0), point!(1.m(), 1.m(), 2.m())); 261 | /// let cuboid2 = Cuboid::from_corners(point!(0.m(), 0.m(), 1.m()), point!(1.m(), 1.m(), 2.m())); 262 | /// assert_eq!( 263 | /// cuboid1.subtract(&cuboid2), 264 | /// Cuboid::from_corners(point!(0, 0, 0), point!(1.m(), 1.m(), 1.m())) 265 | /// ); 266 | /// ``` 267 | pub fn subtract(&self, other: &Self) -> Self { 268 | match (&self.inner, &other.inner) { 269 | (Some(self_inner), Some(other_inner)) => { 270 | let mut fuse_operation = ffi::BRepAlgoAPI_Cut_ctor(self_inner, other_inner); 271 | Self::from_occt(fuse_operation.pin_mut().Shape()) 272 | } 273 | (Some(_), None) => self.clone(), 274 | (None, _) => Part { inner: None }, 275 | } 276 | } 277 | 278 | /// Return the volume occupied by this `Part` in cubic meters. 279 | /// 280 | /// ```rust 281 | /// use anvil::{Cuboid, IntoLength}; 282 | /// 283 | /// let cuboid = Cuboid::from_dim(1.m(), 1.m(), 1.m()); 284 | /// assert!((cuboid.volume() - 1.).abs() < 1e-9) 285 | /// ``` 286 | pub fn volume(&self) -> f64 { 287 | match &self.inner { 288 | Some(inner) => { 289 | let mut gprops = ffi::GProp_GProps_ctor(); 290 | ffi::BRepGProp_VolumeProperties(inner, gprops.pin_mut()); 291 | gprops.Mass() 292 | } 293 | None => 0., 294 | } 295 | } 296 | /// Return the center of mass of the `Part`. 297 | /// 298 | /// If the `Part` is empty, an `Err(Error::EmptyPart)` is returned. 299 | /// 300 | /// ```rust 301 | /// use anvil::{Cuboid, IntoLength, point}; 302 | /// 303 | /// let centered_cuboid = Cuboid::from_dim(1.m(), 1.m(), 1.m()); 304 | /// assert_eq!(centered_cuboid.center(), Ok(point!(0, 0, 0))); 305 | /// 306 | /// let non_centered_cuboid = Cuboid::from_corners( 307 | /// point!(0, 0, 0), 308 | /// point!(2.m(), 2.m(), 2.m()) 309 | /// ); 310 | /// assert_eq!(non_centered_cuboid.center(), Ok(point!(1.m(), 1.m(), 1.m()))); 311 | /// ``` 312 | pub fn center(&self) -> Result, Error> { 313 | match &self.inner { 314 | Some(inner) => { 315 | let mut gprops = ffi::GProp_GProps_ctor(); 316 | ffi::BRepGProp_VolumeProperties(inner, gprops.pin_mut()); 317 | let centre_of_mass = ffi::GProp_GProps_CentreOfMass(&gprops); 318 | 319 | Ok(point!( 320 | Length::from_m(round(centre_of_mass.X(), 9)), 321 | Length::from_m(round(centre_of_mass.Y(), 9)), 322 | Length::from_m(round(centre_of_mass.Z(), 9)) 323 | )) 324 | } 325 | None => Err(Error::EmptyPart), 326 | } 327 | } 328 | 329 | /// Write the `Part` to a file in the STEP format. 330 | pub fn write_step(&self, path: impl AsRef) -> Result<(), Error> { 331 | match &self.scale(1000.).inner { 332 | Some(inner) => { 333 | let mut writer = ffi::STEPControl_Writer_ctor(); 334 | let status = ffi::transfer_shape(writer.pin_mut(), inner); 335 | if status != ffi::IFSelect_ReturnStatus::IFSelect_RetDone { 336 | return Err(Error::StepWrite(path.as_ref().to_path_buf())); 337 | } 338 | let status = ffi::write_step( 339 | writer.pin_mut(), 340 | path.as_ref().to_string_lossy().to_string(), 341 | ); 342 | if status != ffi::IFSelect_ReturnStatus::IFSelect_RetDone { 343 | return Err(Error::StepWrite(path.as_ref().to_path_buf())); 344 | } 345 | } 346 | None => return Err(Error::EmptyPart), 347 | } 348 | Ok(()) 349 | } 350 | 351 | /// Write the `Part` to a file in the STL format. 352 | pub fn write_stl(&self, path: impl AsRef) -> Result<(), Error> { 353 | self.write_stl_with_tolerance(path, 0.0001) 354 | } 355 | 356 | /// Write the `Part` to a file in the STL format with a specified tolerance. 357 | /// 358 | /// Smaller tolerances lead to higher precision in rounded shapes, but also larger file size. 359 | pub fn write_stl_with_tolerance( 360 | &self, 361 | path: impl AsRef, 362 | tolerance: f64, 363 | ) -> Result<(), Error> { 364 | match &self.inner { 365 | Some(inner) => { 366 | let mut writer = ffi::StlAPI_Writer_ctor(); 367 | let mesh = ffi::BRepMesh_IncrementalMesh_ctor(inner, tolerance); 368 | let success = ffi::write_stl( 369 | writer.pin_mut(), 370 | mesh.Shape(), 371 | path.as_ref().to_string_lossy().to_string(), 372 | ); 373 | if success { 374 | Ok(()) 375 | } else { 376 | Err(Error::StlWrite(path.as_ref().to_path_buf())) 377 | } 378 | } 379 | None => Err(Error::EmptyPart), 380 | } 381 | } 382 | /// Return the STL lines that describe this `Part`. 383 | pub fn stl(&self) -> Result, Error> { 384 | match &self.inner { 385 | Some(_) => { 386 | let temp_file = NamedTempFile::new().expect("could not create tempfile"); 387 | let path = temp_file.path(); 388 | 389 | self.write_stl(path)?; 390 | 391 | let file = fs::File::open(path).map_err(|_| Error::StlWrite(path.into()))?; 392 | let lines = io::BufReader::new(file) 393 | .lines() 394 | .collect::, _>>() 395 | .map_err(|_| Error::StlWrite(path.into()))?; 396 | 397 | Ok(lines) 398 | } 399 | None => Err(Error::EmptyPart), 400 | } 401 | } 402 | 403 | pub(crate) fn from_occt(part: &ffi::TopoDS_Shape) -> Self { 404 | let inner = ffi::TopoDS_Shape_to_owned(part); 405 | Self { inner: Some(inner) } 406 | } 407 | } 408 | 409 | impl Clone for Part { 410 | fn clone(&self) -> Self { 411 | match &self.inner { 412 | Some(inner) => Self::from_occt(inner), 413 | None => Part { inner: None }, 414 | } 415 | } 416 | } 417 | 418 | impl PartialEq for Part { 419 | fn eq(&self, other: &Self) -> bool { 420 | match (&self.inner, &other.inner) { 421 | (Some(_), Some(_)) => { 422 | let intersection = self.intersect(other); 423 | 424 | (intersection.volume() - self.volume()).abs() < intersection.volume() * 1e-7 425 | && (intersection.volume() - other.volume()).abs() < intersection.volume() * 1e-7 426 | } 427 | (Some(_), None) => false, 428 | (None, Some(_)) => false, 429 | (None, None) => true, 430 | } 431 | } 432 | } 433 | 434 | impl Debug for Part { 435 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 436 | f.debug_struct("Shape") 437 | .field("stl", &self.stl().expect("")) 438 | .finish() 439 | } 440 | } 441 | 442 | fn round(x: f64, n_digits: u8) -> f64 { 443 | (x * f64::from(10 ^ n_digits)).round() / f64::from(10 ^ n_digits) 444 | } 445 | 446 | #[cfg(test)] 447 | mod tests { 448 | use super::*; 449 | use crate::{Cuboid, IntoLength, Sphere, point}; 450 | 451 | #[test] 452 | fn eq_both_none() { 453 | assert!(Part::empty() == Part::empty()) 454 | } 455 | 456 | #[test] 457 | fn eq_both_cuboid() { 458 | let cuboid1 = Cuboid::from_m(1., 1., 1.); 459 | let cuboid2 = Cuboid::from_m(1., 1., 1.); 460 | assert!(cuboid1 == cuboid2) 461 | } 462 | 463 | #[test] 464 | fn neq_both_cuboid() { 465 | let cuboid1 = Cuboid::from_m(1., 1., 1.); 466 | let cuboid2 = Cuboid::from_m(2., 2., 2.); 467 | assert!(cuboid1 != cuboid2) 468 | } 469 | 470 | #[test] 471 | fn eq_both_sphere() { 472 | let sphere1 = Sphere::from_radius(2.m()); 473 | let sphere2 = Sphere::from_radius(2.m()); 474 | assert!(sphere1 == sphere2) 475 | } 476 | 477 | #[test] 478 | fn neq_both_sphere() { 479 | let sphere1 = Sphere::from_radius(1.m()); 480 | let sphere2 = Sphere::from_radius(2.m()); 481 | assert!(sphere1 != sphere2) 482 | } 483 | 484 | #[test] 485 | fn move_to_deepcopied() { 486 | let cuboid1 = Cuboid::from_m(1., 1., 1.); 487 | let loc = point!(2.m(), 2.m(), 2.m()); 488 | let cuboid2 = cuboid1.move_to(loc); 489 | 490 | assert_eq!(cuboid1.center(), Ok(Point::<3>::origin())); 491 | assert_eq!(cuboid2.center(), Ok(loc)); 492 | } 493 | 494 | #[test] 495 | fn volume() { 496 | let cuboid = Cuboid::from_m(1., 1., 1.); 497 | assert!((cuboid.volume() - 1.).abs() < 1e-9) 498 | } 499 | 500 | #[test] 501 | fn centre_of_mass_at_origin() { 502 | let cuboid = Cuboid::from_m(1., 1., 1.); 503 | assert_eq!(cuboid.center(), Ok(point!(0, 0, 0))) 504 | } 505 | 506 | #[test] 507 | fn centre_of_mass_not_at_origin() { 508 | let cuboid = Cuboid::from_corners(point!(0, 0, 0), point!(2.m(), 2.m(), 2.m())); 509 | assert_eq!(cuboid.center(), Ok(point!(1.m(), 1.m(), 1.m()))) 510 | } 511 | 512 | #[test] 513 | fn part_move_to_twice() { 514 | let part = Cuboid::from_m(1., 1., 1.); 515 | assert_eq!( 516 | part.move_to(point!(1.m(), 1.m(), 1.m())) 517 | .move_to(point!(-1.m(), -1.m(), -1.m())), 518 | Cuboid::from_m(1., 1., 1.).move_to(point!(-1.m(), -1.m(), -1.m())), 519 | ) 520 | } 521 | 522 | #[test] 523 | fn move_after_rotate_should_not_reset_rotate() { 524 | let part = Cuboid::from_m(1., 1., 2.); 525 | assert_eq!( 526 | part.rotate_around(Axis::<3>::y(), 90.deg()) 527 | .move_to(Point::<3>::origin()), 528 | Cuboid::from_m(2., 1., 1.) 529 | ) 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /src/parts/primitives/cube.rs: -------------------------------------------------------------------------------- 1 | use crate::{Cuboid, Length, Part}; 2 | 3 | /// Builder for a cubic `Part`. 4 | /// 5 | /// While the `Cube` struct itself is not used, its constructor methods like `Cube::from_size()` 6 | /// can be used to build this primitive `Part`. 7 | #[derive(Debug, PartialEq, Clone)] 8 | pub struct Cube; 9 | impl Cube { 10 | /// Construct a centered cubic `Part` from the length on every side. 11 | /// 12 | /// # Example 13 | /// ```rust 14 | /// use anvil::{Cube, IntoLength, Part, point}; 15 | /// 16 | /// let part = Cube::from_size(1.m()); 17 | /// assert_eq!(part.center(), Ok(point!(0, 0, 0))); 18 | /// assert!((part.volume() - 1.).abs() < 1e-5); 19 | /// ``` 20 | pub fn from_size(size: Length) -> Part { 21 | Cuboid::from_dim(size, size, size) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/parts/primitives/cuboid.rs: -------------------------------------------------------------------------------- 1 | use opencascade_sys::ffi; 2 | 3 | use crate::{Length, Part, Point, core::is_zero, point}; 4 | 5 | /// Builder for a cuboidal `Part`. 6 | /// 7 | /// While the `Cuboid` struct itself is not used, its constructor methods like `Cuboid::from_dim()` 8 | /// can be used to build this primitive `Part`. 9 | #[derive(Debug, PartialEq, Clone)] 10 | pub struct Cuboid; 11 | impl Cuboid { 12 | /// Construct a centered cuboidal `Part` from the x, y, and z dimensions. 13 | /// 14 | /// # Example 15 | /// ```rust 16 | /// use anvil::{Cuboid, IntoLength, Part, point}; 17 | /// 18 | /// let part = Cuboid::from_dim(1.m(), 2.m(), 3.m()); 19 | /// assert_eq!(part.center(), Ok(point!(0, 0, 0))); 20 | /// assert!((part.volume() - 6.).abs() < 1e-5); 21 | /// ``` 22 | pub fn from_dim(x: Length, y: Length, z: Length) -> Part { 23 | Self::from_corners( 24 | point!(x * -0.5, y * -0.5, z * -0.5), 25 | point!(x * 0.5, y * 0.5, z * 0.5), 26 | ) 27 | } 28 | /// Construct a centered cuboidal `Part` from its corner locations. 29 | /// 30 | /// # Example 31 | /// ```rust 32 | /// use anvil::{Cuboid, IntoLength, Part, point}; 33 | /// 34 | /// let part = Cuboid::from_corners(point!(0, 0, 0), point!(2.m(), 2.m(), 2.m())); 35 | /// assert_eq!(part.center(), Ok(point!(1.m(), 1.m(), 1.m()))); 36 | /// assert!((part.volume() - 8.).abs() < 1e-5); 37 | /// ``` 38 | pub fn from_corners(corner1: Point<3>, corner2: Point<3>) -> Part { 39 | let volume_is_zero = is_zero(&[ 40 | corner1.x() - corner2.x(), 41 | corner1.y() - corner2.y(), 42 | corner1.z() - corner2.z(), 43 | ]); 44 | if volume_is_zero { 45 | return Part::empty(); 46 | } 47 | 48 | let min_x = corner1.x().min(&corner2.x()).m(); 49 | let min_y = corner1.y().min(&corner2.y()).m(); 50 | let min_z = corner1.z().min(&corner2.z()).m(); 51 | let max_x = corner1.x().max(&corner2.x()).m(); 52 | let max_y = corner1.y().max(&corner2.y()).m(); 53 | let max_z = corner1.z().max(&corner2.z()).m(); 54 | 55 | let point = ffi::new_point(min_x, min_y, min_z); 56 | let mut cuboid = 57 | ffi::BRepPrimAPI_MakeBox_ctor(&point, max_x - min_x, max_y - min_y, max_z - min_z); 58 | 59 | Part::from_occt(cuboid.pin_mut().Shape()) 60 | } 61 | /// Construct a centered cuboidal `Part` directly from the x, y, and z meter values. 62 | /// 63 | /// This function is primarily intended to simplify tests and should not be exptected in 64 | /// similar structs. 65 | /// 66 | /// # Example 67 | /// ```rust 68 | /// use anvil::{Cuboid, IntoLength, Part}; 69 | /// 70 | /// assert_eq!( 71 | /// Cuboid::from_m(1., 2., 3.), 72 | /// Cuboid::from_dim(1.m(), 2.m(), 3.m()) 73 | /// ) 74 | /// ``` 75 | pub fn from_m(x: f64, y: f64, z: f64) -> Part { 76 | Self::from_dim(Length::from_m(x), Length::from_m(y), Length::from_m(z)) 77 | } 78 | /// Construct a centered cuboidal `Part` directly from the x, y, and z millimeter values. 79 | /// 80 | /// This function is primarily intended to simplify tests and should not be exptected in 81 | /// similar structs. 82 | /// 83 | /// # Example 84 | /// ```rust 85 | /// use anvil::{Cuboid, IntoLength, Part}; 86 | /// 87 | /// assert_eq!( 88 | /// Cuboid::from_mm(1., 2., 3.), 89 | /// Cuboid::from_dim(1.mm(), 2.mm(), 3.mm()) 90 | /// ) 91 | /// ``` 92 | pub fn from_mm(x: f64, y: f64, z: f64) -> Part { 93 | Self::from_dim(Length::from_mm(x), Length::from_mm(y), Length::from_mm(z)) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | use crate::IntoLength; 101 | 102 | #[test] 103 | fn from_dim_empty() { 104 | assert!(Cuboid::from_dim(0.m(), 1.m(), 1.m()) == Part::empty()); 105 | assert!(Cuboid::from_dim(1.m(), 0.m(), 1.m()) == Part::empty()); 106 | assert!(Cuboid::from_dim(1.m(), 1.m(), 0.m()) == Part::empty()) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/parts/primitives/cylinder.rs: -------------------------------------------------------------------------------- 1 | use crate::{Length, Part, core::is_zero}; 2 | use opencascade_sys::ffi; 3 | 4 | /// Builder for a cylindrical `Part`. 5 | /// 6 | /// While the `Cylinder` struct itself is not used, its constructor methods like 7 | /// `Cylinder::from_radius()` can be used to build this primitive `Part`. 8 | #[derive(Debug, PartialEq, Clone)] 9 | pub struct Cylinder; 10 | impl Cylinder { 11 | /// Construct a centered cylindrical `Part` from a given radius. 12 | /// 13 | /// # Example 14 | /// ```rust 15 | /// use anvil::{Cylinder, IntoLength, Point, Part}; 16 | /// 17 | /// let part = Cylinder::from_radius(1.m(), 2.m()); 18 | /// assert_eq!(part.center(), Ok(Point::<3>::origin())); 19 | /// assert!((part.volume() - 6.28319).abs() < 1e-5); 20 | /// ``` 21 | pub fn from_radius(radius: Length, height: Length) -> Part { 22 | if is_zero(&[radius, height]) { 23 | return Part::empty(); 24 | } 25 | let axis = ffi::gp_Ax2_ctor( 26 | &ffi::new_point(0., 0., -height.m() / 2.), 27 | &ffi::gp_Dir_ctor(0., 0., 1.), 28 | ); 29 | let mut make = ffi::BRepPrimAPI_MakeCylinder_ctor(&axis, radius.m(), height.m()); 30 | Part::from_occt(make.pin_mut().Shape()) 31 | } 32 | 33 | /// Construct a centered cylindrical `Part` from a given diameter. 34 | /// 35 | /// # Example 36 | /// ```rust 37 | /// use anvil::{Cylinder, IntoLength, Point, Part}; 38 | /// 39 | /// let part = Cylinder::from_diameter(1.m(), 2.m()); 40 | /// assert_eq!(part.center(), Ok(Point::<3>::origin())); 41 | /// assert!((part.volume() - 1.57080).abs() < 1e-5); 42 | /// ``` 43 | pub fn from_diameter(diameter: Length, height: Length) -> Part { 44 | Self::from_radius(diameter / 2., height) 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | use crate::IntoLength; 52 | 53 | #[test] 54 | fn from_radius_empty() { 55 | assert!(Cylinder::from_radius(0.m(), 1.m()) == Part::empty()); 56 | assert!(Cylinder::from_radius(1.m(), 0.m()) == Part::empty()); 57 | } 58 | 59 | #[test] 60 | fn from_diameter_empty() { 61 | assert!(Cylinder::from_diameter(0.m(), 1.m()) == Part::empty()); 62 | assert!(Cylinder::from_diameter(1.m(), 0.m()) == Part::empty()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/parts/primitives/mod.rs: -------------------------------------------------------------------------------- 1 | mod cube; 2 | mod cuboid; 3 | mod cylinder; 4 | mod sphere; 5 | 6 | pub use cube::Cube; 7 | pub use cuboid::Cuboid; 8 | pub use cylinder::Cylinder; 9 | pub use sphere::Sphere; 10 | -------------------------------------------------------------------------------- /src/parts/primitives/sphere.rs: -------------------------------------------------------------------------------- 1 | use crate::{Length, Part, core::is_zero}; 2 | use opencascade_sys::ffi; 3 | 4 | /// Builder for a spherical `Part`. 5 | /// 6 | /// While the `Sphere` struct itself is not used, its constructor methods like `Sphere::from_radius()` 7 | /// can be used to build this primitive `Part`. 8 | #[derive(Debug, PartialEq, Clone)] 9 | pub struct Sphere; 10 | impl Sphere { 11 | /// Construct a centered spherical `Part` from a given radius. 12 | /// 13 | /// # Example 14 | /// ```rust 15 | /// use anvil::{Sphere, IntoLength, Point, Part}; 16 | /// 17 | /// let part = Sphere::from_radius(1.m()); 18 | /// assert_eq!(part.center(), Ok(Point::<3>::origin())); 19 | /// assert!((part.volume() - 4.18879).abs() < 1e-5); 20 | /// ``` 21 | pub fn from_radius(radius: Length) -> Part { 22 | if is_zero(&[radius]) { 23 | return Part::empty(); 24 | } 25 | 26 | let axis = ffi::gp_Ax2_ctor(&ffi::new_point(0., 0., 0.), &ffi::gp_Dir_ctor(0., 0., 1.)); 27 | let mut make_sphere = 28 | ffi::BRepPrimAPI_MakeSphere_ctor(&axis, radius.m(), std::f64::consts::TAU); 29 | Part::from_occt(make_sphere.pin_mut().Shape()) 30 | } 31 | /// Construct a centered spherical `Part` from a given diameter. 32 | /// 33 | /// # Example 34 | /// ```rust 35 | /// use anvil::{Sphere, IntoLength, Point, Part}; 36 | /// 37 | /// let part = Sphere::from_diameter(1.m()); 38 | /// assert_eq!(part.center(), Ok(Point::<3>::origin())); 39 | /// assert!((part.volume() - 0.523599).abs() < 1e-5); 40 | /// ``` 41 | pub fn from_diameter(diameter: Length) -> Part { 42 | Self::from_radius(diameter / 2.) 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::*; 49 | use crate::IntoLength; 50 | 51 | #[test] 52 | fn from_radius_empty() { 53 | assert!(Sphere::from_radius(0.m()) == Part::empty()) 54 | } 55 | 56 | #[test] 57 | fn from_diameter_empty() { 58 | assert!(Sphere::from_diameter(0.m()) == Part::empty()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/sketches/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod primitives; 2 | mod sketch; 3 | 4 | pub use sketch::Sketch; 5 | -------------------------------------------------------------------------------- /src/sketches/primitives/circle.rs: -------------------------------------------------------------------------------- 1 | use crate::{Length, Path, Point, Sketch}; 2 | 3 | /// Builder for a circular `Sketch`. 4 | /// 5 | /// While the `Circle` struct itself is not used, its constructor methods like 6 | /// `Circle::from_radius()` can be used to build this primitive `Sketch`. 7 | #[derive(Debug, PartialEq, Clone)] 8 | pub struct Circle; 9 | impl Circle { 10 | /// Construct a centered circular `Sketch` from a given radius. 11 | /// 12 | /// # Example 13 | /// ```rust 14 | /// use anvil::{Circle, IntoLength, Point}; 15 | /// 16 | /// let circle = Circle::from_radius(1.m()); 17 | /// assert!((circle.area() - 3.141593).abs() < 1e-5); 18 | /// assert_eq!(circle.center(), Ok(Point::<2>::origin())); 19 | /// ``` 20 | pub fn from_radius(radius: Length) -> Sketch { 21 | Path::at(Point::<2>::new([radius * -1., Length::zero()])) 22 | .arc_points( 23 | Point::<2>::new([Length::zero(), radius]), 24 | Point::<2>::new([radius, Length::zero()]), 25 | ) 26 | .arc_points( 27 | Point::<2>::new([Length::zero(), radius * -1.]), 28 | Point::<2>::new([radius * -1., Length::zero()]), 29 | ) 30 | .close() 31 | } 32 | 33 | /// Construct a centered circular `Sketch` from a given diameter. 34 | /// 35 | /// # Example 36 | /// ```rust 37 | /// use anvil::{Circle, IntoLength, Point}; 38 | /// 39 | /// let circle = Circle::from_diameter(1.m()); 40 | /// assert!((circle.area() - 0.785398).abs() < 1e-5); 41 | /// assert_eq!(circle.center(), Ok(Point::<2>::origin())); 42 | /// ``` 43 | pub fn from_diameter(diameter: Length) -> Sketch { 44 | Self::from_radius(diameter / 2.) 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | use crate::IntoLength; 52 | 53 | #[test] 54 | fn from_radius_empty() { 55 | assert_eq!(Circle::from_radius(0.m()), Sketch::empty()) 56 | } 57 | 58 | #[test] 59 | fn from_diameter_empty() { 60 | assert_eq!(Circle::from_diameter(0.m()), Sketch::empty()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/sketches/primitives/mod.rs: -------------------------------------------------------------------------------- 1 | mod circle; 2 | mod rectangle; 3 | mod square; 4 | 5 | pub use circle::Circle; 6 | pub use rectangle::Rectangle; 7 | pub use square::Square; 8 | -------------------------------------------------------------------------------- /src/sketches/primitives/rectangle.rs: -------------------------------------------------------------------------------- 1 | use crate::{Length, Path, Point, Sketch, point}; 2 | 3 | /// Builder for a rectangular `Sketch`. 4 | /// 5 | /// While the `Rectangle` struct itself is not used, its constructor methods like 6 | /// `Rectangle::from_dim()` can be used to build this primitive `Sketch`. 7 | #[derive(Debug, PartialEq, Clone)] 8 | pub struct Rectangle; 9 | impl Rectangle { 10 | /// Construct a centered rectangular `Sketch` from the x and y dimensions. 11 | /// 12 | /// # Example 13 | /// ```rust 14 | /// use anvil::{IntoLength, Rectangle, point}; 15 | /// 16 | /// let rect = Rectangle::from_dim(1.m(), 1.m()); 17 | /// assert_eq!(rect.area(), 1.); 18 | /// assert_eq!(rect.center(), Ok(point!(0, 0))); 19 | /// ``` 20 | pub fn from_dim(x: Length, y: Length) -> Sketch { 21 | Self::from_corners(point!(x * -0.5, y * -0.5), point!(x * 0.5, y * 0.5)) 22 | } 23 | 24 | /// Construct a rectangular `Sketch` from its corner locations. 25 | /// 26 | /// # Example 27 | /// ```rust 28 | /// use anvil::{IntoLength, Rectangle, point}; 29 | /// 30 | /// let rect = Rectangle::from_corners(point!(0, 0), point!(2.m(), 2.m())); 31 | /// assert_eq!(rect.area(), 4.); 32 | /// ``` 33 | pub fn from_corners(corner1: Point<2>, corner2: Point<2>) -> Sketch { 34 | if corner1.x() == corner2.x() || corner1.y() == corner2.y() { 35 | return Sketch::empty(); 36 | } 37 | Path::at(corner1) 38 | .line_to(Point::<2>::new([corner2.x(), corner1.y()])) 39 | .line_to(corner2) 40 | .line_to(Point::<2>::new([corner1.x(), corner2.y()])) 41 | .close() 42 | } 43 | 44 | /// Construct a centered rectangular `Sketch` directly from the x and y meter values. 45 | /// 46 | /// This function is primarily intended to simplify tests and should not be exptected in 47 | /// similar structs. 48 | /// 49 | /// # Example 50 | /// ```rust 51 | /// use anvil::{IntoLength, Rectangle}; 52 | /// 53 | /// assert_eq!( 54 | /// Rectangle::from_m(1., 2.), 55 | /// Rectangle::from_dim(1.m(), 2.m()) 56 | /// ) 57 | /// ``` 58 | pub fn from_m(x: f64, y: f64) -> Sketch { 59 | Self::from_dim(Length::from_m(x), Length::from_m(y)) 60 | } 61 | 62 | /// Construct a centered rectangular `Sketch` directly from the x and y millimeter values. 63 | /// 64 | /// This function is primarily intended to simplify tests and should not be exptected in 65 | /// similar structs. 66 | /// 67 | /// # Example 68 | /// ```rust 69 | /// use anvil::{IntoLength, Rectangle}; 70 | /// 71 | /// assert_eq!( 72 | /// Rectangle::from_mm(1., 2.), 73 | /// Rectangle::from_dim(1.mm(), 2.mm()) 74 | /// ) 75 | /// ``` 76 | pub fn from_mm(x: f64, y: f64) -> Sketch { 77 | Self::from_dim(Length::from_mm(x), Length::from_mm(y)) 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use crate::{IntoLength, point}; 85 | 86 | #[test] 87 | fn from_dim_empty() { 88 | assert_eq!(Rectangle::from_dim(0.m(), 1.m()), Sketch::empty()); 89 | assert_eq!(Rectangle::from_dim(1.m(), 0.m()), Sketch::empty()); 90 | } 91 | 92 | #[test] 93 | fn from_corners_empty() { 94 | assert_eq!( 95 | Rectangle::from_corners(point!(1.m(), 2.m()), point!(1.m(), 4.m())), 96 | Sketch::empty() 97 | ); 98 | assert_eq!( 99 | Rectangle::from_corners(point!(1.m(), 2.m()), point!(3.m(), 2.m())), 100 | Sketch::empty() 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/sketches/primitives/square.rs: -------------------------------------------------------------------------------- 1 | use crate::{Length, Rectangle, Sketch}; 2 | 3 | /// Builder for a square `Sketch`. 4 | /// 5 | /// While the `Square` struct itself is not used, its constructor methods like `Square::from_size()` 6 | /// can be used to build this primitive `Sketch`. 7 | #[derive(Debug, PartialEq, Clone)] 8 | pub struct Square; 9 | impl Square { 10 | /// Construct a centered cubic `Sketch` from the length on every side. 11 | /// 12 | /// # Example 13 | /// ```rust 14 | /// use anvil::{Square, IntoLength, Sketch, point}; 15 | /// 16 | /// let Sketch = Square::from_size(1.m()); 17 | /// assert_eq!(Sketch.center(), Ok(point!(0, 0))); 18 | /// assert!((Sketch.area() - 1.).abs() < 1e-5); 19 | /// ``` 20 | pub fn from_size(size: Length) -> Sketch { 21 | Rectangle::from_dim(size, size) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/sketches/sketch.rs: -------------------------------------------------------------------------------- 1 | use std::vec; 2 | 3 | use cxx::UniquePtr; 4 | use opencascade_sys::ffi; 5 | 6 | use crate::{Angle, Axis, Edge, Error, Face, IntoAngle, IntoLength, Length, Part, Plane, Point}; 7 | 8 | /// A closed shape in 2D space. 9 | #[derive(Debug, Clone)] 10 | pub struct Sketch(Vec); 11 | impl Sketch { 12 | /// Construct an empty `Sketch` which can be used for merging with other sketches. 13 | /// 14 | /// ```rust 15 | /// use anvil::Sketch; 16 | /// 17 | /// let sketch = Sketch::empty(); 18 | /// assert_eq!(sketch.area(), 0.); 19 | /// ``` 20 | pub fn empty() -> Self { 21 | Self(vec![]) 22 | } 23 | 24 | /// Return true if this `Sketch` is empty. 25 | pub fn is_empty(&self) -> bool { 26 | self.to_occt(Plane::xy()).is_err() 27 | } 28 | 29 | /// Return the area occupied by this `Sketch` in square meters. 30 | /// 31 | /// Warning: the area is susceptibility to floating point errors. 32 | /// 33 | /// ```rust 34 | /// use anvil::{Rectangle, IntoLength}; 35 | /// 36 | /// let sketch = Rectangle::from_dim(2.m(), 3.m()); 37 | /// assert!((sketch.area() - 6.).abs() < 1e-9) 38 | /// ``` 39 | pub fn area(&self) -> f64 { 40 | match self.to_occt(Plane::xy()) { 41 | Ok(occt) => occt_area(&occt), 42 | Err(_) => 0., 43 | } 44 | } 45 | /// Return the center of mass of the `Sketch`. 46 | /// 47 | /// If the `Sketch` is empty, an `Err(Error::EmptySketch)` is returned. 48 | /// 49 | /// ```rust 50 | /// use anvil::{Error, IntoLength, Rectangle, Sketch, point}; 51 | /// 52 | /// let centered_rect = Rectangle::from_dim(1.m(), 2.m()); 53 | /// let moved_rect = centered_rect.move_to(point!(3.m(), 3.m())); 54 | /// assert_eq!(centered_rect.center(), Ok(point!(0, 0))); 55 | /// assert_eq!(moved_rect.center(), Ok(point!(3.m(), 3.m()))); 56 | /// assert_eq!(Sketch::empty().center(), Err(Error::EmptySketch)); 57 | /// ``` 58 | pub fn center(&self) -> Result, Error> { 59 | let occt = self.to_occt(Plane::xy())?; 60 | let point_3d = occt_center(&occt); 61 | Ok(Point::<2>::new([point_3d.x(), point_3d.y()])) 62 | } 63 | 64 | /// Merge this `Sketch` with another. 65 | /// 66 | /// ```rust 67 | /// use anvil::{IntoLength, Rectangle, point}; 68 | /// 69 | /// let sketch1 = Rectangle::from_corners(point!(0, 0), point!(1.m(), 2.m())); 70 | /// let sketch2 = Rectangle::from_corners(point!(1.m(), 0.m()), point!(2.m(), 2.m())); 71 | /// assert_eq!( 72 | /// sketch1.add(&sketch2), 73 | /// Rectangle::from_corners(point!(0, 0), point!(2.m(), 2.m())) 74 | /// ) 75 | /// ``` 76 | pub fn add(&self, other: &Self) -> Self { 77 | let mut new_actions = self.0.clone(); 78 | new_actions.push(SketchAction::Add(other.clone())); 79 | Self(new_actions) 80 | } 81 | 82 | /// Create multiple instances of the `Sketch` spaced evenly around a point. 83 | /// 84 | /// ```rust 85 | /// use anvil::{IntoAngle, IntoLength, Rectangle, point}; 86 | /// 87 | /// let rect = Rectangle::from_corners(point!(1.m(), 1.m()), point!(2.m(), 2.m())); 88 | /// assert_eq!( 89 | /// rect.circular_pattern(point!(0, 0), 4), 90 | /// rect 91 | /// .add(&rect.rotate_around(point!(0, 0), 90.deg())) 92 | /// .add(&rect.rotate_around(point!(0, 0), 180.deg())) 93 | /// .add(&rect.rotate_around(point!(0, 0), 270.deg())) 94 | /// ) 95 | /// ``` 96 | pub fn circular_pattern(&self, around: Point<2>, instances: u8) -> Self { 97 | let angle_step = 360.deg() / instances as f64; 98 | let mut new_shape = self.clone(); 99 | let mut angle = 0.rad(); 100 | for _ in 0..instances { 101 | new_shape = new_shape.add(&self.rotate_around(around, angle)); 102 | angle = angle + angle_step; 103 | } 104 | new_shape 105 | } 106 | /// Return the `Sketch` that is created from the overlapping area between this one and another. 107 | /// 108 | /// ```rust 109 | /// use anvil::{Rectangle, IntoLength, point}; 110 | /// 111 | /// let sketch1 = Rectangle::from_corners(point!(0, 0), point!(2.m(), 2.m())); 112 | /// let sketch2 = Rectangle::from_corners(point!(0, 0), point!(1.m(), 2.m())); 113 | /// assert_eq!( 114 | /// sketch1.intersect(&sketch2), 115 | /// Rectangle::from_corners(point!(0, 0), point!(1.m(), 2.m())) 116 | /// ) 117 | /// ``` 118 | pub fn intersect(&self, other: &Self) -> Self { 119 | let mut new_actions = self.0.clone(); 120 | new_actions.push(SketchAction::Intersect(other.clone())); 121 | Self(new_actions) 122 | } 123 | 124 | /// Create multiple instances of the `Sketch` spaced evenly until a point. 125 | /// 126 | /// ```rust 127 | /// use anvil::{Rectangle, IntoLength, point}; 128 | /// 129 | /// let rect = Rectangle::from_dim(1.m(), 1.m()); 130 | /// assert_eq!( 131 | /// rect.linear_pattern(point!(4.m(), 0.m()), 5), 132 | /// rect 133 | /// .add(&rect.move_to(point!(1.m(), 0.m()))) 134 | /// .add(&rect.move_to(point!(2.m(), 0.m()))) 135 | /// .add(&rect.move_to(point!(3.m(), 0.m()))) 136 | /// .add(&rect.move_to(point!(4.m(), 0.m()))) 137 | /// ) 138 | /// ``` 139 | pub fn linear_pattern(&self, until: Point<2>, instances: u8) -> Self { 140 | let start = match self.center() { 141 | Ok(p) => p, 142 | Err(_) => return self.clone(), 143 | }; 144 | let axis = match Axis::<2>::between(start, until) { 145 | Ok(axis) => axis, 146 | Err(_) => return self.clone(), 147 | }; 148 | 149 | let len_step = (start - until).distance_to(Point::<2>::origin()) / instances as f64; 150 | let mut new_part = self.clone(); 151 | let mut pos = Length::zero(); 152 | for _ in 0..instances { 153 | pos = pos + len_step; 154 | new_part = new_part.add(&self.move_to(axis.point_at(pos))); 155 | } 156 | new_part 157 | } 158 | /// Return a clone of this `Sketch` moved by a specified amount in each axis. 159 | /// 160 | /// ```rust 161 | /// use anvil::{Circle, IntoLength, point}; 162 | /// 163 | /// let circle = Circle::from_radius(1.m()); 164 | /// let moved_circle = circle 165 | /// .move_by(1.m(), 0.m()) 166 | /// .move_by(0.m(), 2.m()); 167 | /// assert_eq!( 168 | /// moved_circle.center(), 169 | /// Ok(point!(1.m(), 2.m())) 170 | /// ) 171 | /// ``` 172 | pub fn move_by(&self, dx: Length, dy: Length) -> Self { 173 | let center = match self.center() { 174 | Ok(c) => c, 175 | Err(_) => return self.clone(), 176 | }; 177 | self.move_to(center + Point::<2>::new([dx, dy])) 178 | } 179 | /// Return a clone of this `Sketch` moved to a specified point. 180 | /// 181 | /// ```rust 182 | /// use anvil::{IntoLength, Rectangle, point}; 183 | /// 184 | /// let rect = Rectangle::from_dim(1.m(), 1.m()); 185 | /// let moved_rect = rect.move_to(point!(2.m(), 2.m())); 186 | /// assert_eq!(rect.center(), Ok(point!(0, 0))); 187 | /// assert_eq!(moved_rect.center(), Ok(point!(2.m(), 2.m()))); 188 | /// ``` 189 | pub fn move_to(&self, loc: Point<2>) -> Self { 190 | let mut new_actions = self.0.clone(); 191 | new_actions.push(SketchAction::MoveTo(loc)); 192 | Self(new_actions) 193 | } 194 | /// Return a clone of this `Sketch` rotated around its center. 195 | /// 196 | /// Positive angle values result in a counter-clockwise rotation. 197 | /// 198 | /// ```rust 199 | /// use anvil::{IntoAngle, IntoLength, Rectangle, point}; 200 | /// 201 | /// let sketch = Rectangle::from_dim(1.m(), 2.m()).move_to(point!(1.m(), 1.m())); 202 | /// assert_eq!( 203 | /// sketch.rotate(90.deg()), 204 | /// Rectangle::from_dim(2.m(), 1.m()).move_to(point!(1.m(), 1.m())) 205 | /// ) 206 | /// ``` 207 | pub fn rotate(&self, angle: Angle) -> Self { 208 | match self.center() { 209 | Ok(center) => self.rotate_around(center, angle), 210 | Err(_) => self.clone(), 211 | } 212 | } 213 | /// Return a clone of this `Sketch` rotated around its center. 214 | /// 215 | /// Positive angle values result in a counter-clockwise rotation. 216 | /// 217 | /// ```rust 218 | /// use anvil::{IntoAngle, IntoLength, Rectangle, point}; 219 | /// 220 | /// let sketch = Rectangle::from_corners(point!(0, 0), point!(1.m(), 1.m())); 221 | /// assert_eq!( 222 | /// sketch.rotate_around(point!(0, 0), 90.deg()), 223 | /// Rectangle::from_corners(point!(0, 0), point!(-1.m(), 1.m())) 224 | /// ) 225 | /// ``` 226 | pub fn rotate_around(&self, point: Point<2>, angle: Angle) -> Self { 227 | let mut new_actions = self.0.clone(); 228 | new_actions.push(SketchAction::RotateAround(point, angle)); 229 | Self(new_actions) 230 | } 231 | /// Return a clone of this `Sketch` with the size scaled by a factor. 232 | /// 233 | /// # Example 234 | /// ```rust 235 | /// use anvil::{Rectangle, IntoLength}; 236 | /// 237 | /// let rect = Rectangle::from_dim(1.m(), 1.m()); 238 | /// assert_eq!( 239 | /// rect.scale(2.), 240 | /// Rectangle::from_dim(2.m(), 2.m()) 241 | /// ) 242 | /// ``` 243 | pub fn scale(&self, factor: f64) -> Self { 244 | let mut new_actions = self.0.clone(); 245 | new_actions.push(SketchAction::Scale(factor)); 246 | Self(new_actions) 247 | } 248 | /// Return a copy of this `Sketch` with the intersection of another removed. 249 | /// 250 | /// # Example 251 | /// ```rust 252 | /// use anvil::{IntoLength, Rectangle, point}; 253 | /// 254 | /// let sketch1 = Rectangle::from_corners(point!(0, 0), point!(2.m(), 2.m())); 255 | /// let sketch2 = Rectangle::from_corners(point!(1.m(), 0.m()), point!(2.m(), 2.m())); 256 | /// assert_eq!( 257 | /// sketch1.subtract(&sketch2), 258 | /// Rectangle::from_corners(point!(0, 0), point!(1.m(), 2.m())) 259 | /// ) 260 | /// ``` 261 | pub fn subtract(&self, other: &Self) -> Self { 262 | let mut new_actions = self.0.clone(); 263 | new_actions.push(SketchAction::Subtract(other.clone())); 264 | Self(new_actions) 265 | } 266 | 267 | /// Convert this `Sketch` into a `Part` by linearly extruding it. 268 | /// 269 | /// # Example 270 | /// ```rust 271 | /// use anvil::{Cuboid, IntoLength, Rectangle, Plane, point}; 272 | /// 273 | /// let sketch = Rectangle::from_corners(point!(0, 0), point!(1.m(), 2.m())); 274 | /// assert_eq!( 275 | /// sketch.extrude(Plane::xy(), 3.m()), 276 | /// Ok(Cuboid::from_corners(point!(0, 0, 0), point!(1.m(), 2.m(), 3.m()))) 277 | /// ); 278 | /// ``` 279 | pub fn extrude(&self, plane: Plane, thickness: Length) -> Result { 280 | if thickness == Length::zero() { 281 | return Err(Error::EmptySketch); 282 | } 283 | 284 | let shape = self.to_occt(plane)?; 285 | let mut make_solid = ffi::BRepPrimAPI_MakePrism_ctor( 286 | &shape, 287 | &(plane.normal() * thickness).to_occt_vec(), 288 | false, 289 | true, 290 | ); 291 | 292 | Ok(Part::from_occt(make_solid.pin_mut().Shape())) 293 | } 294 | 295 | /// Try to convert this `Sketch` into a `Face`. 296 | pub fn to_face(self, plane: Plane) -> Result { 297 | Ok(Face::from_occt(ffi::TopoDS_cast_to_face( 298 | self.to_occt(plane)?.as_ref().unwrap(), 299 | ))) 300 | } 301 | 302 | pub(crate) fn from_edges(edges: Vec) -> Self { 303 | Self(vec![SketchAction::AddEdges(edges)]) 304 | } 305 | 306 | pub(crate) fn to_occt(&self, plane: Plane) -> Result, Error> { 307 | let mut occt = None; 308 | for action in &self.0 { 309 | occt = action.apply(occt, plane); 310 | } 311 | 312 | match occt { 313 | Some(face) => Ok(face), 314 | None => Err(Error::EmptySketch), 315 | } 316 | } 317 | } 318 | 319 | impl PartialEq for Sketch { 320 | fn eq(&self, other: &Self) -> bool { 321 | if self.center() != other.center() { 322 | return false; 323 | } 324 | 325 | match self.intersect(other).to_occt(Plane::xy()) { 326 | Ok(intersection) => { 327 | (occt_area(&intersection) - self.area()).abs() < 1e-7 328 | && (occt_area(&intersection) - other.area()).abs() < 1e-7 329 | } 330 | Err(_) => true, 331 | } 332 | } 333 | } 334 | 335 | fn edges_to_occt(edges: &[Edge], plane: Plane) -> Result, Error> { 336 | let occt_edges: Vec> = edges 337 | .iter() 338 | .filter_map(|edge| edge.to_occt(plane)) 339 | .collect(); 340 | 341 | if occt_edges.is_empty() { 342 | return Err(Error::EmptySketch); 343 | } 344 | 345 | let mut make_wire = ffi::BRepBuilderAPI_MakeWire_ctor(); 346 | for edge in occt_edges { 347 | make_wire.pin_mut().add_edge(&edge) 348 | } 349 | let wire = ffi::TopoDS_Wire_to_owned(make_wire.pin_mut().Wire()); 350 | 351 | let make_face = ffi::BRepBuilderAPI_MakeFace_wire(&wire, false); 352 | let face = make_face.Face(); 353 | Ok(ffi::TopoDS_Shape_to_owned(ffi::cast_face_to_shape(face))) 354 | } 355 | 356 | fn occt_area(occt: &ffi::TopoDS_Shape) -> f64 { 357 | let mut gprops = ffi::GProp_GProps_ctor(); 358 | ffi::BRepGProp_SurfaceProperties(occt, gprops.pin_mut()); 359 | gprops.Mass() 360 | } 361 | 362 | fn occt_center(occt: &ffi::TopoDS_Shape) -> Point<3> { 363 | let mut gprops = ffi::GProp_GProps_ctor(); 364 | ffi::BRepGProp_VolumeProperties(occt, gprops.pin_mut()); 365 | 366 | let centre_of_mass = ffi::GProp_GProps_CentreOfMass(&gprops); 367 | Point::<3>::new([ 368 | centre_of_mass.X().m(), 369 | centre_of_mass.Y().m(), 370 | centre_of_mass.X().m(), 371 | ]) 372 | } 373 | 374 | #[derive(Debug, PartialEq, Clone)] 375 | enum SketchAction { 376 | Add(Sketch), 377 | AddEdges(Vec), 378 | Intersect(Sketch), 379 | MoveTo(Point<2>), 380 | RotateAround(Point<2>, Angle), 381 | Scale(f64), 382 | Subtract(Sketch), 383 | } 384 | impl SketchAction { 385 | pub fn apply( 386 | &self, 387 | sketch: Option>, 388 | plane: Plane, 389 | ) -> Option> { 390 | match self { 391 | SketchAction::Add(other) => match (sketch, other.to_occt(plane).ok()) { 392 | (None, None) => None, 393 | (None, Some(other)) => Some(other), 394 | (Some(sketch), None) => Some(sketch), 395 | (Some(self_shape), Some(other_shape)) => { 396 | let mut operation = ffi::BRepAlgoAPI_Fuse_ctor(&self_shape, &other_shape); 397 | Some(ffi::TopoDS_Shape_to_owned(operation.pin_mut().Shape())) 398 | } 399 | }, 400 | SketchAction::AddEdges(edges) => edges_to_occt(edges, plane).ok(), 401 | SketchAction::Intersect(other) => match (sketch, other.to_occt(plane).ok()) { 402 | (Some(self_shape), Some(other_shape)) => { 403 | let mut operation = ffi::BRepAlgoAPI_Common_ctor(&self_shape, &other_shape); 404 | let new_shape = ffi::TopoDS_Shape_to_owned(operation.pin_mut().Shape()); 405 | if occt_area(&new_shape) == 0. { 406 | None 407 | } else { 408 | Some(new_shape) 409 | } 410 | } 411 | _ => None, 412 | }, 413 | SketchAction::MoveTo(loc) => match sketch { 414 | Some(shape) => { 415 | let mut transform = ffi::new_transform(); 416 | transform 417 | .pin_mut() 418 | .set_translation_vec(&loc.to_3d(plane).to_occt_vec()); 419 | let location = ffi::TopLoc_Location_from_transform(&transform); 420 | 421 | let mut new_inner = ffi::TopoDS_Shape_to_owned(&shape); 422 | new_inner.pin_mut().set_global_translation(&location, false); 423 | 424 | Some(new_inner) 425 | } 426 | None => None, 427 | }, 428 | SketchAction::RotateAround(point, angle) => match sketch { 429 | Some(shape) => { 430 | let mut transform = ffi::new_transform(); 431 | transform.pin_mut().SetRotation( 432 | &Axis::<3> { 433 | origin: point.to_3d(plane), 434 | direction: plane.normal(), 435 | } 436 | .to_occt_ax1(), 437 | angle.rad(), 438 | ); 439 | let mut operation = 440 | ffi::BRepBuilderAPI_Transform_ctor(&shape, &transform, false); 441 | let new_shape = ffi::TopoDS_Shape_to_owned(operation.pin_mut().Shape()); 442 | Some(new_shape) 443 | } 444 | None => None, 445 | }, 446 | SketchAction::Scale(factor) => match sketch { 447 | Some(shape) => { 448 | let mut transform = ffi::new_transform(); 449 | transform 450 | .pin_mut() 451 | .SetScale(&occt_center(&shape).to_occt_point(), *factor); 452 | let mut operation = 453 | ffi::BRepBuilderAPI_Transform_ctor(&shape, &transform, false); 454 | let new_shape = ffi::TopoDS_Shape_to_owned(operation.pin_mut().Shape()); 455 | Some(new_shape) 456 | } 457 | None => None, 458 | }, 459 | SketchAction::Subtract(other) => match (sketch, other.to_occt(plane).ok()) { 460 | (None, None) => None, 461 | (None, Some(_)) => None, 462 | (Some(sketch), None) => Some(sketch), 463 | (Some(self_shape), Some(other_shape)) => { 464 | let mut operation = ffi::BRepAlgoAPI_Cut_ctor(&self_shape, &other_shape); 465 | Some(ffi::TopoDS_Shape_to_owned(operation.pin_mut().Shape())) 466 | } 467 | }, 468 | } 469 | } 470 | } 471 | 472 | #[cfg(test)] 473 | mod tests { 474 | use crate::{ 475 | Cuboid, Cylinder, IntoLength, Path, Point, Rectangle, point, sketches::primitives::Circle, 476 | }; 477 | 478 | use super::*; 479 | 480 | #[test] 481 | fn eq_both_rectangles() { 482 | assert_eq!( 483 | Rectangle::from_dim(1.m(), 1.m()), 484 | Rectangle::from_dim(1.m(), 1.m()), 485 | ) 486 | } 487 | 488 | #[test] 489 | fn ne_both_rectangles() { 490 | assert_ne!( 491 | Rectangle::from_dim(1.m(), 1.m()), 492 | Rectangle::from_dim(1.m(), 1.1.m()), 493 | ) 494 | } 495 | 496 | #[test] 497 | fn eq_both_rectangles_not_at_origin() { 498 | assert_eq!( 499 | Rectangle::from_dim(1.m(), 1.m()).move_to(point!(2.m(), 2.m())), 500 | Rectangle::from_dim(1.m(), 1.m()).move_to(point!(2.m(), 2.m())), 501 | ) 502 | } 503 | 504 | #[test] 505 | fn ne_both_rectangles_not_at_origin() { 506 | assert_ne!( 507 | Rectangle::from_dim(1.m(), 1.m()).move_to(point!(2.m(), 2.m())), 508 | Rectangle::from_dim(1.m(), 1.m()).move_to(point!(3.m(), 3.m())), 509 | ) 510 | } 511 | 512 | #[test] 513 | fn eq_both_rectangles_rotated() { 514 | assert_eq!( 515 | Rectangle::from_dim(1.m(), 1.m()).rotate(45.deg()), 516 | Rectangle::from_dim(1.m(), 1.m()).rotate(45.deg()), 517 | ) 518 | } 519 | 520 | #[test] 521 | fn ne_both_rectangles_rotated() { 522 | assert_ne!( 523 | Rectangle::from_dim(1.m(), 1.m()).rotate(45.deg()), 524 | Rectangle::from_dim(1.m(), 1.m()).rotate(90.deg()), 525 | ) 526 | } 527 | 528 | #[test] 529 | fn ne_different_sketches() { 530 | assert_ne!( 531 | Rectangle::from_dim(1.m(), 1.m()).move_to(point!(2.m(), 2.m())), 532 | Circle::from_radius(1.m()).move_to(point!(2.m(), 2.m())), 533 | ) 534 | } 535 | 536 | #[test] 537 | fn intersect_non_overlapping() { 538 | let sketch1 = Rectangle::from_corners(point!(1.m(), 1.m()), point!(2.m(), 2.m())); 539 | let sketch2 = Rectangle::from_corners(point!(-1.m(), -1.m()), point!(-2.m(), -2.m())); 540 | assert!(sketch1.intersect(&sketch2).to_occt(Plane::xy()).is_err()) 541 | } 542 | 543 | #[test] 544 | fn extrude_empty_sketch() { 545 | let sketch = Sketch::empty(); 546 | assert_eq!(sketch.extrude(Plane::xy(), 5.m()), Err(Error::EmptySketch)) 547 | } 548 | 549 | #[test] 550 | fn extrude_zero_thickness() { 551 | let sketch = Rectangle::from_dim(1.m(), 2.m()); 552 | assert_eq!( 553 | sketch.extrude(Plane::xy(), Length::zero()), 554 | Err(Error::EmptySketch) 555 | ) 556 | } 557 | 558 | #[test] 559 | fn extrude_cube_different_plane() { 560 | let sketch = Path::at(point!(0, 0)) 561 | .line_to(point!(1.m(), 0.m())) 562 | .line_to(point!(1.m(), 2.m())) 563 | .line_to(point!(0.m(), 2.m())) 564 | .close(); 565 | assert_eq!( 566 | sketch.extrude(Plane::xz(), Length::from_m(-3.)), 567 | Ok(Cuboid::from_corners( 568 | Point::<3>::origin(), 569 | point!(1.m(), 3.m(), 2.m()) 570 | )) 571 | ) 572 | } 573 | 574 | #[test] 575 | fn extrude_cylinder() { 576 | let sketch = Circle::from_radius(1.m()); 577 | assert_eq!( 578 | sketch.extrude(Plane::xy(), 2.m()), 579 | Ok(Cylinder::from_radius(1.m(), 2.m()).move_to(point!(0.m(), 0.m(), 1.m()))) 580 | ) 581 | } 582 | } 583 | --------------------------------------------------------------------------------