├── .github ├── actions-rs │ └── grcov.yml └── workflows │ ├── coverage.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── diagrams ├── circle.svg ├── cubic-bezier.svg └── reed-leaf.svg └── src ├── algebra.rs ├── curve.rs ├── knotvec.rs ├── lib.rs └── main.rs /.github/actions-rs/grcov.yml: -------------------------------------------------------------------------------- 1 | # Re-enable branch and excl-br-line when this is released: 2 | # https://github.com/actions-rs/grcov/pull/90 3 | 4 | # branch: true 5 | ignore-not-existing: true 6 | output-path: coverage 7 | # excl-br-line: "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [ main, devel ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | grcov: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Install Rust nightly 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | profile: minimal 25 | toolchain: nightly 26 | override: true 27 | 28 | - name: Clean 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: clean 32 | 33 | - name: Run tests 34 | uses: actions-rs/cargo@v1 35 | with: 36 | command: test 37 | args: --all 38 | env: 39 | CI_GRCOV: '1' 40 | CARGO_INCREMENTAL: 0 41 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort' 42 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort' 43 | 44 | - name: Run grcov 45 | id: grcov 46 | uses: actions-rs/grcov@v0.1 47 | with: 48 | config: .github/actions-rs/grcov.yml 49 | 50 | - name: Upload code coverage to codecov.io 51 | uses: codecov/codecov-action@v1.0.2 52 | with: 53 | file: ${{steps.grcov.outputs.report}} 54 | token: ${{secrets.CODECOV_TOKEN}} 55 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust Build 2 | 3 | on: 4 | push: 5 | branches: [ main, devel ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | 19 | - uses: actions/checkout@v2 20 | 21 | - name: Install latest Rust nightly 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: nightly 25 | override: true 26 | components: rustfmt, clippy 27 | 28 | - name: Compile 29 | run: cargo build --verbose 30 | 31 | - name: Test 32 | run: cargo test --verbose 33 | 34 | - name: Rustfmt check 35 | run: cargo fmt -- --check 36 | 37 | - name: Clippy check 38 | run: cargo clippy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Please track all notable changes in this file. This format is based on 4 | [Keep a Changelog](https://keepachangelog.com/en/1.0.0). 5 | 6 | ## [Unreleased] 7 | 8 | ## [0.0.3] 9 | 10 | ### Added 11 | 12 | - `KnotVec.is_clamped` function. 13 | - "Reed leaf hieroglyph" example of a curve with multiple Bézier segments. 14 | 15 | ### Changed 16 | 17 | - Changed the representation to use full-multiplicity knots instead of the 18 | "Rhino" style. 19 | - `KnotVec` is now passed to `Curve` on creation, instead of an arbitrary 20 | `Vec`. 21 | 22 | ## [0.0.2] 23 | 24 | ### Added 25 | 26 | - `ScalarT` and `VectorT` traits to represent the operations required of 27 | scalars and vectors used by NURBS curves and surfaces. 28 | - `KnotVec` type to represent knot vectors in NURBS curves. This will later be 29 | shared with NURBS surfaces. 30 | - `codecov.io` test coverage using `grcov`. 31 | - README badges for `codecov.io`, `crates.io`, `docs.rs` and the LICENSE. 32 | 33 | ### Changed 34 | 35 | - `Curve` type is now parameterised by scalar and vector types. 36 | - Examples in `main` use `nalgebra::Vector2` instead of 3D points. 37 | 38 | ## [0.0.1] 39 | 40 | ### Added 41 | 42 | - Initial project setup. 43 | - NURBS Curve representation and evaluation using the de Boors algorithm. 44 | - Plot some examples for the README. 45 | 46 | [unreleased]: https://github.com/lancelet/capstan/compare/v0.0.3...HEAD 47 | [0.0.3]: https://github.com/lancelet/capstan/releases/tag/v0.0.3 48 | [0.0.2]: https://github.com/lancelet/capstan/releases/tag/v0.0.2 49 | [0.0.1]: https://github.com/lancelet/capstan/releases/tag/v0.0.1 -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "alga" 5 | version = "0.9.3" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "4f823d037a7ec6ea2197046bafd4ae150e6bc36f9ca347404f46a46823fa84f2" 8 | dependencies = [ 9 | "approx", 10 | "num-complex", 11 | "num-traits", 12 | ] 13 | 14 | [[package]] 15 | name = "approx" 16 | version = "0.3.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" 19 | dependencies = [ 20 | "num-traits", 21 | ] 22 | 23 | [[package]] 24 | name = "autocfg" 25 | version = "1.0.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 28 | 29 | [[package]] 30 | name = "bit-set" 31 | version = "0.5.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" 34 | dependencies = [ 35 | "bit-vec", 36 | ] 37 | 38 | [[package]] 39 | name = "bit-vec" 40 | version = "0.6.2" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "5f0dc55f2d8a1a85650ac47858bb001b4c0dd73d79e3c455a842925e68d29cd3" 43 | 44 | [[package]] 45 | name = "bitflags" 46 | version = "1.2.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 49 | 50 | [[package]] 51 | name = "byteorder" 52 | version = "1.3.4" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 55 | 56 | [[package]] 57 | name = "capstan" 58 | version = "0.0.3" 59 | dependencies = [ 60 | "alga", 61 | "approx", 62 | "nalgebra", 63 | "num-traits", 64 | "proptest", 65 | "svg", 66 | "thiserror", 67 | ] 68 | 69 | [[package]] 70 | name = "cfg-if" 71 | version = "0.1.10" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 74 | 75 | [[package]] 76 | name = "fnv" 77 | version = "1.0.7" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 80 | 81 | [[package]] 82 | name = "generic-array" 83 | version = "0.13.2" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd" 86 | dependencies = [ 87 | "typenum", 88 | ] 89 | 90 | [[package]] 91 | name = "getrandom" 92 | version = "0.1.15" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" 95 | dependencies = [ 96 | "cfg-if", 97 | "libc", 98 | "wasi", 99 | ] 100 | 101 | [[package]] 102 | name = "lazy_static" 103 | version = "1.4.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 106 | 107 | [[package]] 108 | name = "libc" 109 | version = "0.2.78" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98" 112 | 113 | [[package]] 114 | name = "libm" 115 | version = "0.2.1" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" 118 | 119 | [[package]] 120 | name = "matrixmultiply" 121 | version = "0.2.3" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "d4f7ec66360130972f34830bfad9ef05c6610a43938a467bcc9ab9369ab3478f" 124 | dependencies = [ 125 | "rawpointer", 126 | ] 127 | 128 | [[package]] 129 | name = "nalgebra" 130 | version = "0.22.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "4a3f0b89b0a44cb7bb9b62c5e6fd485145ddc6bc14483ab005355e96029b3fbf" 133 | dependencies = [ 134 | "approx", 135 | "generic-array", 136 | "matrixmultiply", 137 | "num-complex", 138 | "num-rational", 139 | "num-traits", 140 | "rand", 141 | "rand_distr", 142 | "simba", 143 | "typenum", 144 | ] 145 | 146 | [[package]] 147 | name = "num-complex" 148 | version = "0.2.4" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" 151 | dependencies = [ 152 | "autocfg", 153 | "num-traits", 154 | ] 155 | 156 | [[package]] 157 | name = "num-integer" 158 | version = "0.1.43" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" 161 | dependencies = [ 162 | "autocfg", 163 | "num-traits", 164 | ] 165 | 166 | [[package]] 167 | name = "num-rational" 168 | version = "0.2.4" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" 171 | dependencies = [ 172 | "autocfg", 173 | "num-integer", 174 | "num-traits", 175 | ] 176 | 177 | [[package]] 178 | name = "num-traits" 179 | version = "0.2.12" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 182 | dependencies = [ 183 | "autocfg", 184 | "libm", 185 | ] 186 | 187 | [[package]] 188 | name = "paste" 189 | version = "0.1.18" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" 192 | dependencies = [ 193 | "paste-impl", 194 | "proc-macro-hack", 195 | ] 196 | 197 | [[package]] 198 | name = "paste-impl" 199 | version = "0.1.18" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" 202 | dependencies = [ 203 | "proc-macro-hack", 204 | ] 205 | 206 | [[package]] 207 | name = "ppv-lite86" 208 | version = "0.2.9" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" 211 | 212 | [[package]] 213 | name = "proc-macro-hack" 214 | version = "0.5.18" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" 217 | 218 | [[package]] 219 | name = "proc-macro2" 220 | version = "1.0.24" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 223 | dependencies = [ 224 | "unicode-xid", 225 | ] 226 | 227 | [[package]] 228 | name = "proptest" 229 | version = "0.10.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "12e6c80c1139113c28ee4670dc50cc42915228b51f56a9e407f0ec60f966646f" 232 | dependencies = [ 233 | "bit-set", 234 | "bitflags", 235 | "byteorder", 236 | "lazy_static", 237 | "num-traits", 238 | "quick-error", 239 | "rand", 240 | "rand_chacha", 241 | "rand_xorshift", 242 | "regex-syntax", 243 | "rusty-fork", 244 | "tempfile", 245 | ] 246 | 247 | [[package]] 248 | name = "quick-error" 249 | version = "1.2.3" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 252 | 253 | [[package]] 254 | name = "quote" 255 | version = "1.0.7" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 258 | dependencies = [ 259 | "proc-macro2", 260 | ] 261 | 262 | [[package]] 263 | name = "rand" 264 | version = "0.7.3" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 267 | dependencies = [ 268 | "getrandom", 269 | "libc", 270 | "rand_chacha", 271 | "rand_core", 272 | "rand_hc", 273 | ] 274 | 275 | [[package]] 276 | name = "rand_chacha" 277 | version = "0.2.2" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 280 | dependencies = [ 281 | "ppv-lite86", 282 | "rand_core", 283 | ] 284 | 285 | [[package]] 286 | name = "rand_core" 287 | version = "0.5.1" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 290 | dependencies = [ 291 | "getrandom", 292 | ] 293 | 294 | [[package]] 295 | name = "rand_distr" 296 | version = "0.2.2" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" 299 | dependencies = [ 300 | "rand", 301 | ] 302 | 303 | [[package]] 304 | name = "rand_hc" 305 | version = "0.2.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 308 | dependencies = [ 309 | "rand_core", 310 | ] 311 | 312 | [[package]] 313 | name = "rand_xorshift" 314 | version = "0.2.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" 317 | dependencies = [ 318 | "rand_core", 319 | ] 320 | 321 | [[package]] 322 | name = "rawpointer" 323 | version = "0.2.1" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" 326 | 327 | [[package]] 328 | name = "redox_syscall" 329 | version = "0.1.57" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 332 | 333 | [[package]] 334 | name = "regex-syntax" 335 | version = "0.6.18" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 338 | 339 | [[package]] 340 | name = "remove_dir_all" 341 | version = "0.5.3" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 344 | dependencies = [ 345 | "winapi", 346 | ] 347 | 348 | [[package]] 349 | name = "rusty-fork" 350 | version = "0.3.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" 353 | dependencies = [ 354 | "fnv", 355 | "quick-error", 356 | "tempfile", 357 | "wait-timeout", 358 | ] 359 | 360 | [[package]] 361 | name = "simba" 362 | version = "0.2.1" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "cdec3fb717e5504ecbef1cf4223c334a215f95323092afeae57125ec40e4995b" 365 | dependencies = [ 366 | "approx", 367 | "num-complex", 368 | "num-traits", 369 | "paste", 370 | ] 371 | 372 | [[package]] 373 | name = "svg" 374 | version = "0.8.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "0b65a64d32a41db2a8081aa03c1ccca26f246ff681add693f8b01307b137da79" 377 | 378 | [[package]] 379 | name = "syn" 380 | version = "1.0.42" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" 383 | dependencies = [ 384 | "proc-macro2", 385 | "quote", 386 | "unicode-xid", 387 | ] 388 | 389 | [[package]] 390 | name = "tempfile" 391 | version = "3.1.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 394 | dependencies = [ 395 | "cfg-if", 396 | "libc", 397 | "rand", 398 | "redox_syscall", 399 | "remove_dir_all", 400 | "winapi", 401 | ] 402 | 403 | [[package]] 404 | name = "thiserror" 405 | version = "1.0.20" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" 408 | dependencies = [ 409 | "thiserror-impl", 410 | ] 411 | 412 | [[package]] 413 | name = "thiserror-impl" 414 | version = "1.0.20" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" 417 | dependencies = [ 418 | "proc-macro2", 419 | "quote", 420 | "syn", 421 | ] 422 | 423 | [[package]] 424 | name = "typenum" 425 | version = "1.12.0" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" 428 | 429 | [[package]] 430 | name = "unicode-xid" 431 | version = "0.2.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 434 | 435 | [[package]] 436 | name = "wait-timeout" 437 | version = "0.2.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 440 | dependencies = [ 441 | "libc", 442 | ] 443 | 444 | [[package]] 445 | name = "wasi" 446 | version = "0.9.0+wasi-snapshot-preview1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 449 | 450 | [[package]] 451 | name = "winapi" 452 | version = "0.3.9" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 455 | dependencies = [ 456 | "winapi-i686-pc-windows-gnu", 457 | "winapi-x86_64-pc-windows-gnu", 458 | ] 459 | 460 | [[package]] 461 | name = "winapi-i686-pc-windows-gnu" 462 | version = "0.4.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 465 | 466 | [[package]] 467 | name = "winapi-x86_64-pc-windows-gnu" 468 | version = "0.4.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 471 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "capstan" 3 | version = "0.0.3" 4 | authors = ["Jonathan Merritt "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "NURBS library with a CAD focus" 8 | homepage = "https://github.com/lancelet/capstan/" 9 | repository = "https://github.com/lancelet/capstan/" 10 | documentation = "https://docs.rs/capstan" 11 | keywords = ["NURBS", "graphics", "CAD"] 12 | categories = ["algorithms", "graphics", "mathematics"] 13 | readme = "README.md" 14 | 15 | [dependencies] 16 | alga = "0.9" 17 | approx = "0.3" 18 | nalgebra = "0.22" 19 | num-traits = "0.2" 20 | svg = "0.8" 21 | thiserror = "1.0" 22 | 23 | [dev-dependencies] 24 | proptest = "0.10" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Jonathan Merritt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capstan 2 | 3 | ![GitHub Rust CI](https://github.com/lancelet/capstan/workflows/Rust/badge.svg) 4 | [![Codecov.io](https://codecov.io/gh/lancelet/capstan/branch/main/graph/badge.svg)](https://codecov.io/gh/lancelet/capstan) 5 | [![Crates.io](https://img.shields.io/crates/v/capstan.svg)](https://crates.io/crates/capstan) 6 | [![Docs.rs](https://docs.rs/capstan/badge.svg)](https://docs.rs/capstan) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | NURBS utilities in Rust. 10 | 11 | ## NURBS Curve Evaluation 12 | 13 | Currently, only NURBS curve evaluation is complete. The evaluation uses a 14 | naive version of the de Boor algorithm. With this, it's possible to evaluate 15 | the 3D coordinates of a NURBS curve at any parameter value. 16 | 17 | NURBS can represent conics with floating-point precision. This image shows a 18 | tesselated NURBS circle on the left and an SVG circle on the right: 19 | 20 | 21 | 22 | NURBS are a generalization of Bézier curves, so they can exactly represent any 23 | order of Bézier curve. The image below shows an SVG cubic Bézier with a loop on 24 | the right and a tesselated NURBS representation on the left: 25 | 26 | 27 | 28 | NURBS can represent multiple Bézier curve segments in a single curve. The 29 | example below shows an outline of the Egyptian "reed leaf" hieroglyph 30 | (Gardiner sign M17). This curve is constructed from 2 line segments and 4 31 | cubic Bézier curves, all of which can be represented as a single closed 32 | NURBS curve: 33 | 34 | 35 | 36 | ## NURBS Curve Representation 37 | 38 | The library uses the "Rhino" form of NURBS curves, where there are two fewer 39 | knots than in "traditional" NURBS. -------------------------------------------------------------------------------- /diagrams/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /diagrams/cubic-bezier.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /diagrams/reed-leaf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/algebra.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::base::allocator::Allocator; 2 | use nalgebra::base::{DefaultAllocator, DimName, VectorN}; 3 | use num_traits::identities::One; 4 | use std::fmt::Debug; 5 | use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub}; 6 | 7 | /// A scalar type. 8 | /// 9 | /// Scalars are used for things like knot locations, weights, parameter values, 10 | /// and the scalar components of vector types. 11 | pub trait ScalarT: 12 | Copy 13 | + PartialOrd 14 | + Debug 15 | + Add 16 | + AddAssign 17 | + Mul 18 | + MulAssign 19 | + Sub 20 | + Div 21 | + One 22 | { 23 | } 24 | 25 | impl ScalarT for T where 26 | T: Copy 27 | + PartialOrd 28 | + Debug 29 | + Add 30 | + AddAssign 31 | + Mul 32 | + MulAssign 33 | + Sub 34 | + Div 35 | + One 36 | { 37 | } 38 | 39 | /// A vector type. 40 | /// 41 | /// Vectors are used for 3D locations like control points and points on curves 42 | /// or surfaces. 43 | pub trait VectorT: 44 | Clone + Debug + Add + Mul<::Field, Output = Self> 45 | { 46 | type Field: ScalarT; 47 | } 48 | 49 | impl VectorT for VectorN 50 | where 51 | N: 'static + ScalarT, 52 | D: DimName, 53 | DefaultAllocator: Allocator, 54 | { 55 | type Field = N; 56 | } 57 | -------------------------------------------------------------------------------- /src/curve.rs: -------------------------------------------------------------------------------- 1 | use crate::algebra::{ScalarT, VectorT}; 2 | use crate::knotvec::KnotVec; 3 | use thiserror::Error; 4 | 5 | pub type Result = std::result::Result; 6 | 7 | /// NURBS curve. 8 | /// 9 | /// Non-Uniform Rational B-Spline. 10 | #[derive(PartialEq, Debug)] 11 | pub struct Curve 12 | where 13 | N: ScalarT, 14 | V: VectorT, 15 | { 16 | degree: usize, 17 | control_points: Vec, 18 | weights: Vec, 19 | knots: KnotVec, 20 | } 21 | 22 | impl Curve 23 | where 24 | N: ScalarT, 25 | V: VectorT, 26 | { 27 | /// Creates a new NURBS Curve. 28 | /// 29 | /// The following basic properties must be satisfied for a NURBS curve: 30 | /// * `degree` > 0 31 | /// * `control_points.len() > degree` 32 | /// * `weights.len() == control_points.len()` 33 | /// * `knots.len() == degree + control_points.len() + 1` 34 | /// * `knots.is_clamped()` 35 | /// 36 | /// The NURBS curves represented here are clamped (ie. they must have a 37 | /// knot multiplicity at either end equal to the degree plus one). 38 | /// Un-clamped curves can be converted to clamped ones via knot insertion. 39 | /// 40 | /// Parameters: 41 | /// 42 | /// * `degree` - polynomial degree of the NURBS curve 43 | /// * `control_points` - vector of control points 44 | /// * `weights` - vector of weights (must be the same length as 45 | /// `control_points`) 46 | /// * `knots` - knot vector (must have `degree + control_points.len() + 1` 47 | /// elements) 48 | pub fn new( 49 | degree: usize, 50 | control_points: Vec, 51 | weights: Vec, 52 | knots: KnotVec, 53 | ) -> Result { 54 | if degree == 0 { 55 | Err(CurveError::InvalidDegree) 56 | } else if control_points.len() <= degree { 57 | Err(CurveError::InsufficientControlPoints { 58 | degree, 59 | number_supplied: control_points.len(), 60 | }) 61 | } else if weights.len() != control_points.len() { 62 | Err(CurveError::MismatchedWeightsAndControlPoints) 63 | } else if knots.len() != degree + control_points.len() + 1 { 64 | Err(CurveError::InvalidKnotCount { 65 | required_knot_len: degree + control_points.len() + 1, 66 | receieved_knot_len: knots.len(), 67 | }) 68 | } else if !knots.is_clamped(degree) { 69 | Err(CurveError::KnotVectorNotClamped) 70 | } else { 71 | Ok(Curve { 72 | degree, 73 | control_points, 74 | weights, 75 | knots, 76 | }) 77 | } 78 | } 79 | 80 | /// Interpolates the curve at a parameter value. 81 | /// 82 | /// This method uses the 83 | /// [de Boor algorithm](https://en.wikipedia.org/wiki/De_Boor%27s_algorithm) 84 | /// to evaluate the NURBS curve at a given parameter value `u`. The de Boor 85 | /// algorithm is a good choice for efficiently evaluating a NURBS curve and 86 | /// is numerically stable. 87 | /// 88 | /// The parameter `u` is clamped to the allowed range of the parameter 89 | /// space of the curve (which is the range from `self.knots().min_u()` to 90 | /// `self.knots().max_u()` inclusive). 91 | /// 92 | /// # Parameters 93 | /// 94 | /// * `u` - the parameter value at which to evaluate the NURBS curve 95 | pub fn de_boor(&self, u: N) -> V { 96 | // clamp u and find the knot span containing u 97 | let uu = self.knots.clamp(u); 98 | let k = self.knots.find_span(uu); 99 | 100 | // populate initial triangular column 101 | let mut d = Vec::::with_capacity(self.degree + 1); // homogeneous points 102 | let mut dw = Vec::::with_capacity(self.degree + 1); // weights 103 | for j in 0..self.degree + 1 { 104 | let i: usize = j + k - self.degree; 105 | 106 | // multiply the control points by the corresponding weight to 107 | // convert from Cartesian to homogeneous coordinates 108 | d.push(self.control_points[i].clone() * self.weights[i]); 109 | dw.push(self.weights[i]); 110 | } 111 | 112 | // make extra-sure we allocated enough capacity 113 | debug_assert!(d.len() <= self.degree + 1); 114 | debug_assert!(dw.len() <= self.degree + 1); 115 | 116 | // main de Boor algorithm 117 | for r in 1..self.degree + 1 { 118 | for j in (r..self.degree + 1).rev() { 119 | let kp = self.knots[j + k - self.degree]; 120 | let alpha = (uu - kp) / (self.knots[1 + j + k - r] - kp); 121 | let nalpha = N::one() - alpha; 122 | d[j] = d[j - 1].clone() * nalpha + d[j].clone() * alpha; 123 | dw[j] = dw[j - 1] * nalpha + dw[j] * alpha; 124 | } 125 | } 126 | 127 | // convert final coordinate from homogeneous to Cartesian coords 128 | d[self.degree].clone() * (N::one() / dw[self.degree]) 129 | } 130 | 131 | /// Returns the vector of control points. 132 | pub fn control_points(&self) -> &Vec { 133 | &self.control_points 134 | } 135 | 136 | /// Returns the knot vector. 137 | pub fn knots(&self) -> &KnotVec { 138 | &self.knots 139 | } 140 | 141 | /// Scale the curve by a uniform amount about the origin. 142 | /// 143 | /// NOTE: This method will probably be replaced by a more general 144 | /// transformation method in the future. 145 | pub fn uniform_scale(&mut self, scale_factor: N) { 146 | for cp in &mut self.control_points { 147 | *cp = cp.clone() * scale_factor; 148 | } 149 | } 150 | } 151 | 152 | #[derive(Error, Debug, PartialEq)] 153 | pub enum CurveError { 154 | #[error("invalid degree; must satisfy degree > 0")] 155 | InvalidDegree, 156 | 157 | #[error("N={} control points were supplied; at least {} are required \ 158 | for a degree {} curve", 159 | .number_supplied, 160 | .degree, 161 | .degree - 1)] 162 | InsufficientControlPoints { 163 | degree: usize, 164 | number_supplied: usize, 165 | }, 166 | 167 | #[error("number of weights and control points must be identical")] 168 | MismatchedWeightsAndControlPoints, 169 | 170 | #[error("expected {} knot values, but received {}", 171 | .required_knot_len, 172 | .receieved_knot_len)] 173 | InvalidKnotCount { 174 | required_knot_len: usize, 175 | receieved_knot_len: usize, 176 | }, 177 | 178 | #[error("knot vector was not clamped")] 179 | KnotVectorNotClamped, 180 | } 181 | 182 | #[cfg(test)] 183 | mod tests { 184 | use super::*; 185 | use approx::assert_relative_eq; 186 | use nalgebra::Vector2; 187 | 188 | /// Test Curve 189 | type TC = Curve>; 190 | 191 | /// The degree of a NURBS curve must be >= 0. 192 | #[test] 193 | fn invalid_degree() { 194 | let result = TC::new(0, vec![], vec![], KnotVec::new(vec![0.0, 1.0]).unwrap()); 195 | assert_eq!(result, Err(CurveError::InvalidDegree)); 196 | } 197 | 198 | /// There must be at least degree + 1 control points. 199 | #[test] 200 | fn insufficient_control_points() { 201 | let result = TC::new( 202 | 1, 203 | vec![Vector2::new(0.0, 0.0)], 204 | vec![1.0], 205 | KnotVec::new(vec![0.0, 0.0, 1.0, 1.0]).unwrap(), 206 | ); 207 | assert_eq!( 208 | result, 209 | Err(CurveError::InsufficientControlPoints { 210 | degree: 1, 211 | number_supplied: 1 212 | }) 213 | ) 214 | } 215 | 216 | /// The number of control points and weights must be identical. 217 | #[test] 218 | fn weights_and_cps_lengths_must_be_equal() { 219 | let result = TC::new( 220 | 1, 221 | vec![Vector2::new(0.0, 0.0), Vector2::new(42.0, 56.0)], 222 | vec![1.0], 223 | KnotVec::new(vec![0.0, 1.0]).unwrap(), 224 | ); 225 | assert_eq!(result, Err(CurveError::MismatchedWeightsAndControlPoints)); 226 | } 227 | 228 | /// The correct number of knot values must be supplied. 229 | #[test] 230 | fn invalid_knot_count() { 231 | let result = TC::new( 232 | 1, 233 | vec![Vector2::new(0.0, 0.0), Vector2::new(42.0, 56.0)], 234 | vec![1.0, 1.0], 235 | KnotVec::new(vec![0.0, 1.0]).unwrap(), 236 | ); 237 | assert_eq!( 238 | result, 239 | Err(CurveError::InvalidKnotCount { 240 | required_knot_len: 4, 241 | receieved_knot_len: 2 242 | }) 243 | ); 244 | } 245 | 246 | /// Test that we detect a non-clamped knot vector. 247 | #[test] 248 | fn knot_vector_not_clamped() { 249 | let result = TC::new( 250 | 2, 251 | vec![ 252 | Vector2::new(0.0, 0.0), 253 | Vector2::new(1.0, 2.0), 254 | Vector2::new(3.0, 4.0), 255 | ], 256 | vec![1.0, 1.0, 1.0], 257 | KnotVec::new(vec![0.0, 0.0, 0.5, 0.5, 0.9, 1.0]).unwrap(), 258 | ); 259 | assert_eq!(result, Err(CurveError::KnotVectorNotClamped)); 260 | } 261 | 262 | /// Creating a new NURBS curve successfully. 263 | #[test] 264 | fn new() { 265 | let nurbs = TC::new( 266 | 1, 267 | vec![Vector2::new(0.0, 0.0), Vector2::new(42.0, 56.0)], 268 | vec![1.0, 1.0], 269 | KnotVec::new(vec![0.0, 0.0, 1.0, 1.0]).unwrap(), 270 | ) 271 | .unwrap(); 272 | assert_eq!(nurbs.knots().min_u(), 0.0); 273 | assert_eq!(nurbs.knots().max_u(), 1.0); 274 | assert_eq!( 275 | nurbs.control_points(), 276 | &vec![Vector2::new(0.0, 0.0), Vector2::new(42.0, 56.0)] 277 | ); 278 | } 279 | 280 | /// Uniformly scaling a NURBS curve. 281 | #[test] 282 | fn uniform_scale() { 283 | let mut nurbs = TC::new( 284 | 1, 285 | vec![Vector2::new(0.0, 0.0), Vector2::new(42.0, 56.0)], 286 | vec![1.0, 1.0], 287 | KnotVec::new(vec![0.0, 0.0, 1.0, 1.0]).unwrap(), 288 | ) 289 | .unwrap(); 290 | nurbs.uniform_scale(2.0); 291 | 292 | let expected = TC::new( 293 | 1, 294 | vec![Vector2::new(0.0, 0.0), Vector2::new(2.0 * 42.0, 2.0 * 56.0)], 295 | vec![1.0, 1.0], 296 | KnotVec::new(vec![0.0, 0.0, 1.0, 1.0]).unwrap(), 297 | ) 298 | .unwrap(); 299 | 300 | assert_eq!(nurbs, expected); 301 | } 302 | 303 | /// Test de Boor evalutaion on a non-rational, uniform Bezier. 304 | #[test] 305 | fn de_boor_non_rational_uniform_bezier() { 306 | let test_curve = TC::new( 307 | 3, 308 | vec![ 309 | Vector2::new(-10.0, 10.0), 310 | Vector2::new(10.0, 10.0), 311 | Vector2::new(-10.0, -10.0), 312 | Vector2::new(10.0, -10.0), 313 | ], 314 | vec![1.0, 1.0, 1.0, 1.0], 315 | KnotVec::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]).unwrap(), 316 | ) 317 | .unwrap(); 318 | 319 | // tests for in-range parameter 320 | assert_relative_eq!(Vector2::new(-10.0, 10.0), test_curve.de_boor(0.0)); 321 | assert_relative_eq!(Vector2::new(-2.16, 7.92), test_curve.de_boor(0.2)); 322 | assert_relative_eq!(Vector2::new(0.0, 0.0), test_curve.de_boor(0.5)); 323 | assert_relative_eq!(Vector2::new(10.0, -10.0), test_curve.de_boor(1.0)); 324 | 325 | // tests with parameter out-of-range (clipped to parameter range) 326 | assert_relative_eq!(Vector2::new(-10.0, 10.0), test_curve.de_boor(-1.0)); 327 | assert_relative_eq!(Vector2::new(10.0, -10.0), test_curve.de_boor(2.0)); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/knotvec.rs: -------------------------------------------------------------------------------- 1 | use crate::algebra::ScalarT; 2 | use std::fmt::Debug; 3 | use std::ops::Index; 4 | 5 | /// Vector of knots in non-decreasing order. 6 | /// 7 | /// Knot values exist in the parameter space of a NURBS curve. They partition 8 | /// the total 1D parameter range into regions over which the interpolating 9 | /// polynomials of the NURBS curve are active. Thus, in combination with the 10 | /// degree of a NURBS curve, they define the non-uniform B-spline basis 11 | /// functions. 12 | #[derive(Clone, Debug, PartialEq)] 13 | pub struct KnotVec { 14 | knots: Vec, 15 | } 16 | 17 | impl KnotVec { 18 | /// Creates a new knot vector if possible. 19 | /// 20 | /// A new knot vector must satisfy the following criteria: 21 | /// * it must contain >= 2 elements 22 | /// * it must be sorted (non-decreasing) 23 | /// * it must represent a non-zero span (the last knot cannot be equal to 24 | /// the first) 25 | /// 26 | /// # Parameters 27 | /// 28 | /// * `knots` - a vector of knot values in non-decreasing order 29 | /// 30 | /// # Example 31 | /// 32 | /// ``` 33 | /// # use capstan::knotvec::KnotVec; 34 | /// let knots = KnotVec::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap(); 35 | /// ``` 36 | pub fn new(knots: Vec) -> Option { 37 | if knots.len() >= 2 && knots.is_sorted() && &knots[0] != knots.last().unwrap() { 38 | Some(KnotVec { knots }) 39 | } else { 40 | None 41 | } 42 | } 43 | 44 | /// Returns the number of knots in the knot vector. 45 | /// 46 | /// # Example 47 | /// 48 | /// ``` 49 | /// # use capstan::knotvec::KnotVec; 50 | /// let knots = KnotVec::new(vec![0.0, 0.0, 1.0, 1.0]).unwrap(); 51 | /// assert_eq!(knots.len(), 4); 52 | /// ``` 53 | pub fn len(&self) -> usize { 54 | self.knots.len() 55 | } 56 | 57 | /// Checks if a knot vector is clamped. 58 | /// 59 | /// A knot vector is clamped if the first knot value is repeated 60 | /// `degree + 1` times at the start of the knot vector (ie. its 61 | /// multiplicity is `degree + 1`), and if the last knot is repeated 62 | /// `degree + 1` times at the end of the knot vector. 63 | /// 64 | /// # Parameters 65 | /// 66 | /// * `degree` - degree of the NURBS curve 67 | pub fn is_clamped(&self, degree: usize) -> bool { 68 | if self.knots.len() < 2 * (degree + 1) { 69 | false 70 | } else { 71 | // check the value of the start knots 72 | let start_knot = self.knots[0]; 73 | for i_knot in &self.knots[1..degree] { 74 | if *i_knot != start_knot { 75 | return false; 76 | } 77 | } 78 | 79 | // check the value of the end knots 80 | let end_knot = self.knots.last().unwrap(); 81 | for e_knot in &self.knots[self.knots.len() - degree - 1..self.knots.len() - 1] { 82 | if e_knot != end_knot { 83 | return false; 84 | } 85 | } 86 | 87 | // everything passed 88 | true 89 | } 90 | } 91 | 92 | /// Checks if the knot vector is empty (always returns `false`). 93 | pub fn is_empty(&self) -> bool { 94 | false 95 | } 96 | 97 | /// Returns the minimum parameter value contained in this knot vector. 98 | pub fn min_u(&self) -> N { 99 | self.knots[0] 100 | } 101 | 102 | /// Returns the maximum parameter value contained in this knot vector. 103 | pub fn max_u(&self) -> N { 104 | *self 105 | .knots 106 | .last() 107 | .expect("last() should always succeed, because there should be >=2 knots") 108 | } 109 | 110 | /// Clamp a parameter value to the allowed range of the parameter. 111 | /// 112 | /// # Parameters 113 | /// 114 | /// * `u` - the parameter value to clamp in the range `min_u <= u <= max_u` 115 | pub fn clamp(&self, u: N) -> N { 116 | if u < self.min_u() { 117 | self.min_u() 118 | } else if u > self.max_u() { 119 | self.max_u() 120 | } else { 121 | u 122 | } 123 | } 124 | 125 | /// Finds the index of the span inside the knot vector which contains the 126 | /// parameter value `u`. 127 | /// 128 | /// Each pair of knots in the knot vector defines a span. Spans are 129 | /// zero length for knots with a multiplicity > 1. Given a parameter 130 | /// value, `u`, this function returns the index of the knot span which 131 | /// contains `u`. 132 | /// 133 | /// The knot span index, `i = knots.find_span(u)`, which contains u, 134 | /// satisfies the relationship that: 135 | /// 136 | /// ```text 137 | /// knots[i] <= u < knots[i+1], when u < knots.max_u() 138 | /// knots[i] < u == knots[i+1], when u == knots.max_u() 139 | /// ``` 140 | /// 141 | /// # Parameters 142 | /// 143 | /// * `u` - the parameter value for which to find the span 144 | /// 145 | /// # Examples 146 | /// 147 | /// ``` 148 | /// # use capstan::knotvec::KnotVec; 149 | /// let knots = KnotVec::new(vec![0.0, 0.0, 0.5, 1.0, 1.0]).unwrap(); 150 | /// assert_eq!(knots.find_span(0.0), 1); 151 | /// assert_eq!(knots.find_span(0.6), 2); 152 | /// assert_eq!(knots.find_span(1.0), 2); // note u=knots.max_u() is a bit special 153 | /// ``` 154 | /// 155 | /// # Panics 156 | /// 157 | /// Panics if the parameter `u` is outside the allowed range of the knot 158 | /// vector. 159 | pub fn find_span(&self, u: N) -> usize { 160 | debug_assert!( 161 | u >= self.min_u(), 162 | "parameter u={:?} is below the required range {:?} <= u <= {:?}", 163 | u, 164 | self.min_u(), 165 | self.max_u() 166 | ); 167 | debug_assert!( 168 | u <= self.max_u(), 169 | "parameter u={:?} is above the required range {:?} <= u <= {:?}", 170 | u, 171 | self.min_u(), 172 | self.max_u() 173 | ); 174 | 175 | if u == self.max_u() { 176 | // if we have the maximum u value then handle that as a special case; 177 | // look backward through the knots until we find one which is less 178 | // than the maximum u value 179 | self.knots 180 | .iter() 181 | .enumerate() 182 | .rev() 183 | .find(|&item| item.1 < &u) 184 | .unwrap() 185 | .0 186 | } else { 187 | // perform a binary search to find the correct knot span 188 | let mut low: usize = 0; 189 | let mut high: usize = self.len() - 1; 190 | let mut mid: usize = (low + high) / 2; 191 | 192 | while u < self.knots[mid] || u >= self.knots[mid + 1] { 193 | if u < self.knots[mid] { 194 | high = mid; 195 | } else { 196 | low = mid; 197 | } 198 | mid = (low + high) / 2; 199 | } 200 | 201 | mid 202 | } 203 | } 204 | } 205 | 206 | impl Index for KnotVec { 207 | type Output = N; 208 | 209 | fn index(&self, i: usize) -> &Self::Output { 210 | &self.knots[i] 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | use super::*; 217 | use proptest::prelude::*; 218 | 219 | /// Test creating a new knot vector. 220 | #[test] 221 | fn new() { 222 | let knots = KnotVec::new(vec![0.0, 0.0, 0.5, 1.0, 1.0]).unwrap(); 223 | assert_eq!(knots.len(), 5); 224 | assert_eq!(knots.is_empty(), false); 225 | assert_eq!(knots[0], 0.0); 226 | assert_eq!(knots[1], 0.0); 227 | assert_eq!(knots[2], 0.5); 228 | assert_eq!(knots[3], 1.0); 229 | assert_eq!(knots[4], 1.0); 230 | assert_eq!(knots.min_u(), 0.0); 231 | assert_eq!(knots.max_u(), 1.0); 232 | } 233 | 234 | /// Test the is_clamped method. 235 | #[test] 236 | fn is_clamped() { 237 | let knots1 = KnotVec::new(vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap(); 238 | assert!(knots1.is_clamped(2)); 239 | assert!(!knots1.is_clamped(3)); 240 | 241 | let knots2 = KnotVec::new(vec![0.0, 0.0, 0.0, 1.0, 1.0]).unwrap(); 242 | assert!(knots2.is_clamped(1)); 243 | assert!(!knots2.is_clamped(2)); 244 | assert!(!knots2.is_clamped(100)); 245 | } 246 | 247 | /// Test clamping the paramter. 248 | #[test] 249 | fn clamp() { 250 | let knots = KnotVec::new(vec![0.0, 0.0, 1.0, 1.0]).unwrap(); 251 | assert_eq!(knots.clamp(-1.0), 0.0); 252 | assert_eq!(knots.clamp(0.5), 0.5); 253 | assert_eq!(knots.clamp(1.2), 1.0); 254 | } 255 | 256 | /// A knot vector must always have at least two knots, so that it has a 257 | /// span. 258 | #[test] 259 | fn less_than_two_knots() { 260 | assert_eq!(KnotVec::new(vec![0.0]), None); 261 | } 262 | 263 | /// Knots must be in non-decreasing order. 264 | #[test] 265 | fn badly_ordered_knots() { 266 | assert_eq!(KnotVec::new(vec![1.0, 0.0]), None); 267 | } 268 | 269 | /// Knots cannot be degenerate; they must span some non-zero range. 270 | #[test] 271 | fn degenerate_knots() { 272 | assert_eq!(KnotVec::new(vec![0.0, 0.0, 0.0]), None); 273 | } 274 | 275 | /// Test finding the knot span that contains a parameter value. 276 | #[test] 277 | fn find_span() { 278 | let knots = KnotVec::new(vec![0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 4.0, 5.0, 5.0]).unwrap(); 279 | assert_eq!(knots.find_span(0.0), 1); 280 | assert_eq!(knots.find_span(3.001), 4); 281 | assert_eq!(knots.find_span(4.0), 6); 282 | assert_eq!(knots.find_span(5.0), 6); 283 | } 284 | 285 | #[test] 286 | #[should_panic(expected = "parameter u=0.5 is below the required range 1.0 <= u <= 5.0")] 287 | fn find_span_panic_when_u_is_too_low() { 288 | let knots = KnotVec::new(vec![1.0, 1.0, 1.0, 5.0, 5.0, 5.0]).unwrap(); 289 | knots.find_span(0.5); 290 | } 291 | 292 | #[test] 293 | #[should_panic(expected = "parameter u=5.5 is above the required range 1.0 <= u <= 5.0")] 294 | fn find_span_panic_when_u_is_too_high() { 295 | let knots = KnotVec::new(vec![1.0, 1.0, 1.0, 5.0, 5.0, 5.0]).unwrap(); 296 | knots.find_span(5.5); 297 | } 298 | 299 | prop_compose! { 300 | fn arb_knotvec(min_len: usize) 301 | (len in min_len..128) 302 | (mut ks in proptest::collection::vec(any::(), len)) -> KnotVec 303 | { 304 | // make sure the knot vector is sorted 305 | ks.sort_by(|a, b| a.partial_cmp(b).unwrap()); 306 | 307 | // make sure the knot vector is non-degenerate (it must contain more 308 | // than one unique value) 309 | if ks.last().unwrap() == &ks[0] { 310 | // if it's degenerate, add an extra, non-degenerate knot on the end 311 | ks.push(ks.last().unwrap() + 1.0); 312 | } 313 | 314 | KnotVec::new(ks).unwrap() 315 | } 316 | } 317 | 318 | prop_compose! { 319 | fn arb_knotvec_and_param() 320 | (knotvec in arb_knotvec(2)) 321 | (u in knotvec.min_u()..knotvec.max_u(), knotvec in Just(knotvec)) -> (f32, KnotVec) 322 | { 323 | (u, knotvec) 324 | } 325 | } 326 | 327 | proptest! { 328 | /// For an arbitrary knot vector and parameter value, the span index 329 | /// found for the parameter must actually contain that parameter value. 330 | #[test] 331 | fn knot_span_contains_knot((u, knotvec) in arb_knotvec_and_param()) { 332 | let i: usize = knotvec.find_span(u); 333 | assert!(knotvec[i] <= u); 334 | if u < knotvec.max_u() { 335 | assert!(knotvec[i+1] >= u); 336 | } else { 337 | assert!(knotvec[i+1] > u); 338 | } 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(is_sorted)] 2 | 3 | pub mod algebra; 4 | pub mod curve; 5 | pub mod knotvec; 6 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate svg; 2 | 3 | use nalgebra::Vector2; 4 | use svg::node::element::path; 5 | use svg::node::element::Circle; 6 | use svg::node::element::Group; 7 | use svg::node::element::Path; 8 | use svg::node::Node; 9 | use svg::Document; 10 | 11 | use capstan::knotvec::KnotVec; 12 | type Curve = capstan::curve::Curve>; 13 | 14 | fn main() { 15 | println!("Plotting some examples"); 16 | 17 | circle_example(&String::from("circle.svg")); 18 | cubic_bezier_example(&String::from("cubic-bezier.svg")); 19 | reed_leaf_example(&String::from("reed-leaf.svg")); 20 | } 21 | 22 | fn reed_leaf_example(filename: &str) { 23 | let n_div = 256; 24 | let nurbs_and_cp_group = 25 | curve_and_control_polygon(&reed_leaf(), n_div).set("transform", "translate(-60,0)"); 26 | let nurbs_group = curve_path(&reed_leaf(), n_div).set("transform", "translate(90, 0)"); 27 | 28 | let document = Document::new() 29 | .set("width", 300) 30 | .set("height", 300) 31 | .add(nurbs_and_cp_group) 32 | .add(nurbs_group); 33 | 34 | svg::save(filename, &document).unwrap(); 35 | } 36 | 37 | fn circle_example(filename: &str) { 38 | let radius = 130.0; 39 | 40 | let mut nurbs_circle = unit_circle(); 41 | nurbs_circle.uniform_scale(radius); 42 | let nurbs_group = 43 | curve_and_control_polygon(&nurbs_circle, 256).set("transform", "translate(150, 150)"); 44 | 45 | let circle = style_regular(Circle::new().set("cx", 0).set("cy", 0).set("r", radius)) 46 | .set("transform", "translate(450, 150)"); 47 | 48 | let document = Document::new() 49 | .set("width", 600) 50 | .set("height", 300) 51 | .add(nurbs_group) 52 | .add(circle); 53 | 54 | svg::save(filename, &document).unwrap(); 55 | } 56 | 57 | fn cubic_bezier_example(filename: &str) { 58 | let bezier = style_regular( 59 | Path::new().set( 60 | "d", 61 | path::Data::new() 62 | .move_to((80, 20)) 63 | .cubic_curve_to((280, 280, 20, 280, 220, 20)), 64 | ), 65 | ) 66 | .set("transform", "translate(300, 0)"); 67 | 68 | let nurb_bezier = curve_and_control_polygon( 69 | &Curve::new( 70 | 3, 71 | vec![ 72 | Vector2::new(80.0, 20.0), 73 | Vector2::new(280.0, 280.0), 74 | Vector2::new(20.0, 280.0), 75 | Vector2::new(220.0, 20.0), 76 | ], 77 | vec![1.0, 1.0, 1.0, 1.0], 78 | KnotVec::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]).unwrap(), 79 | ) 80 | .unwrap(), 81 | 256, 82 | ); 83 | 84 | let document = Document::new() 85 | .set("width", 600) 86 | .set("height", 300) 87 | .add(nurb_bezier) 88 | .add(bezier); 89 | 90 | svg::save(filename, &document).unwrap(); 91 | } 92 | 93 | fn style_regular(node: T) -> Group 94 | where 95 | T: Node, 96 | { 97 | Group::new() 98 | .add(node) 99 | .set("fill", "none") 100 | .set("stroke", "blue") 101 | .set("stroke-width", "2px") 102 | .set("vector-effect", "non-scaling-stroke") 103 | } 104 | 105 | fn curve_and_control_polygon(curve: &Curve, n_divisions: usize) -> Group { 106 | Group::new() 107 | .add(curve_path(curve, n_divisions)) 108 | .add(curve_polygon(curve)) 109 | } 110 | 111 | fn curve_path(curve: &Curve, n_divisions: usize) -> Path { 112 | Path::new() 113 | .set("d", curve_path_data(curve, n_divisions)) 114 | .set("fill", "none") 115 | .set("stroke", "#711081") 116 | .set("stroke-width", "2px") 117 | .set("vector-effect", "non-scaling-stroke") 118 | } 119 | 120 | fn curve_path_data(curve: &Curve, n_divisions: usize) -> path::Data { 121 | let min_u = curve.knots().min_u(); 122 | let max_u = curve.knots().max_u(); 123 | let u_range = max_u - min_u; 124 | let range_denom = n_divisions as f32; 125 | 126 | let mut commands = Vec::with_capacity(n_divisions + 1); 127 | commands.push(path::Command::Move( 128 | path::Position::Absolute, 129 | path::Parameters::from(eval_curve_2d(&curve, min_u)), 130 | )); 131 | for i in 1..(n_divisions + 1) { 132 | let u = min_u + (i as f32) * u_range / range_denom; 133 | commands.push(path::Command::Line( 134 | path::Position::Absolute, 135 | path::Parameters::from(eval_curve_2d(&curve, u)), 136 | )) 137 | } 138 | 139 | path::Data::from(commands) 140 | } 141 | 142 | fn curve_polygon(curve: &Curve) -> Group { 143 | // control points 144 | let cps = curve.control_points(); 145 | 146 | // a group for the control points 147 | let mut control_points_group = Group::new(); 148 | for cp in cps { 149 | let cp_circle = control_point(cp.x, cp.y, 3.5); 150 | control_points_group = control_points_group.add(cp_circle); 151 | } 152 | 153 | // the control polygon lines 154 | let mut commands = Vec::with_capacity(cps.len()); 155 | commands.push(path::Command::Move( 156 | path::Position::Absolute, 157 | path::Parameters::from((cps[0].x, cps[0].y)), 158 | )); 159 | for cp in cps.iter().skip(1) { 160 | commands.push(path::Command::Line( 161 | path::Position::Absolute, 162 | path::Parameters::from((cp.x, cp.y)), 163 | )); 164 | } 165 | let path_data = path::Data::from(commands); 166 | let path = Path::new() 167 | .set("d", path_data) 168 | .set("fill", "none") 169 | .set("stroke", "#101010") 170 | .set("stroke-width", "1px") 171 | .set("stroke-dasharray", "4 3") 172 | .set("vector-effect", "non-scaling-stroke"); 173 | 174 | Group::new().add(path).add(control_points_group) 175 | } 176 | 177 | fn control_point(x: f32, y: f32, radius: f32) -> Circle { 178 | Circle::new() 179 | .set("cx", x) 180 | .set("cy", y) 181 | .set("r", radius) 182 | .set("fill", "#AAAAAA") 183 | .set("stroke", "#000000") 184 | .set("stroke-width", "1px") 185 | .set("vector-effect", "non-scaling-stroke") 186 | } 187 | 188 | fn eval_curve_2d(curve: &Curve, u: f32) -> (f32, f32) { 189 | let pt_3d = curve.de_boor(u); 190 | (pt_3d.x, pt_3d.y) 191 | } 192 | 193 | fn unit_circle() -> Curve { 194 | let r = f32::sqrt(2.0) / 2.0; 195 | let degree = 2; 196 | let control_points = vec![ 197 | Vector2::new(1.0, 0.0), 198 | Vector2::new(1.0, 1.0), 199 | Vector2::new(0.0, 1.0), 200 | Vector2::new(-1.0, 1.0), 201 | Vector2::new(-1.0, 0.0), 202 | Vector2::new(-1.0, -1.0), 203 | Vector2::new(0.0, -1.0), 204 | Vector2::new(1.0, -1.0), 205 | Vector2::new(1.0, 0.0), 206 | ]; 207 | let weights = vec![1.0, r, 1.0, r, 1.0, r, 1.0, r, 1.0]; 208 | let knots = KnotVec::new(vec![ 209 | 0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0, 210 | ]) 211 | .unwrap(); 212 | Curve::new(degree, control_points, weights, knots).unwrap() 213 | } 214 | 215 | fn reed_leaf() -> Curve { 216 | let degree = 3; 217 | let control_points = vec![ 218 | Vector2::new(152.0, 18.0), 219 | Vector2::new(140.0, 24.0), 220 | Vector2::new(130.0, 29.0), 221 | Vector2::new(121.0, 41.0), 222 | Vector2::new(105.0, 65.0), 223 | Vector2::new(105.0, 96.0), 224 | Vector2::new(107.0, 282.0), 225 | Vector2::new(107.0, 282.0), 226 | Vector2::new(125.0, 277.0), 227 | Vector2::new(125.0, 277.0), 228 | Vector2::new(125.0, 267.0), 229 | Vector2::new(123.0, 235.0), 230 | Vector2::new(129.0, 230.0), 231 | Vector2::new(140.0, 221.0), 232 | Vector2::new(158.0, 209.0), 233 | Vector2::new(173.0, 201.0), 234 | Vector2::new(173.0, 201.0), 235 | Vector2::new(152.0, 18.0), 236 | Vector2::new(152.0, 18.0), 237 | ]; 238 | let weights = vec![ 239 | 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 240 | 1.0, 241 | ]; 242 | let knots = KnotVec::new(vec![ 243 | 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0, 4.0, 5.0, 5.0, 244 | 5.0, 6.0, 6.0, 6.0, 6.0, 245 | ]) 246 | .unwrap(); 247 | Curve::new(degree, control_points, weights, knots).unwrap() 248 | } 249 | --------------------------------------------------------------------------------