diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fd22d32 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +Please track all notable changes in this file. This format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.0.0). + +## [Unreleased] + +## [0.0.1] + +### Added + +- Initial project setup. +- NURBS Curve representation and evaluation using the de Boors algorithm. +- Plot some examples for the README. + +[unreleased]: https://github.com/lancelet/capstan/compare/v0.0.1...HEAD +[0.0.1]: https://github.com/lancelet/capstan/releases/tag/v0.0.1 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 86c6b98..2213e7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,316 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "alga" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f823d037a7ec6ea2197046bafd4ae150e6bc36f9ca347404f46a46823fa84f2" +dependencies = [ + "approx", + "num-complex", + "num-traits", +] + +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "capstan" -version = "0.1.0" +version = "0.0.1" +dependencies = [ + "alga", + "approx", + "nalgebra", + "svg", + "thiserror", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "generic-array" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd" +dependencies = [ + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98" + +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "matrixmultiply" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4f7ec66360130972f34830bfad9ef05c6610a43938a467bcc9ab9369ab3478f" +dependencies = [ + "rawpointer", +] + +[[package]] +name = "nalgebra" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3f0b89b0a44cb7bb9b62c5e6fd485145ddc6bc14483ab005355e96029b3fbf" +dependencies = [ + "approx", + "generic-array", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "rand", + "rand_distr", + "simba", + "typenum", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" + +[[package]] +name = "proc-macro-hack" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" +dependencies = [ + "rand", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "simba" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1585d831b5c904e42c4df7a4fcfa03e4b56a8cfa445aff0a04f4effe397ecac9" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", +] + +[[package]] +name = "svg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b65a64d32a41db2a8081aa03c1ccca26f246ff681add693f8b01307b137da79" + +[[package]] +name = "syn" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" diff --git a/Cargo.toml b/Cargo.toml index 0fec277..737e71e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,20 @@ [package] name = "capstan" -version = "0.1.0" +version = "0.0.1" authors = ["Jonathan Merritt "] edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +license = "MIT" +description = "NURBS library with a CAD focus" +homepage = "https://github.com/lancelet/capstan/" +repository = "https://github.com/lancelet/capstan/" +documentation = "https://docs.rs/capstan" +keywords = ["NURBS", "graphics", "CAD"] +categories = ["algorithms", "graphics", "mathematics"] +readme = "README.md" [dependencies] +alga = "0.9" +approx = "0.3" +nalgebra = "0.22" +svg = "0.8" +thiserror = "1.0" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..734527c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Jonathan Merritt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index baf4446..e875feb 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,26 @@ ![GitHub Rust CI](https://github.com/lancelet/capstan/workflows/Rust/badge.svg) -NURBS utilities in Rust. \ No newline at end of file +NURBS utilities in Rust. + +## NURBS Curve Evaluation + +Currently, only NURBS curve evaluation is complete. The evaluation uses a +naive version of the de Boor algorithm. With this, it's possible to evaluate +the 3D coordinates of a NURBS curve at any parameter value. + +NURBS can represent conics with floating-point precision. This image shows a +tesselated NURBS circle on the left and an SVG circle on the right: + + + +NURBS are a generalization of Bézier curves, so they can exactly represent any +order of Bézier curve. The image below shows an SVG cubic Bézier with a loop on +the right and a tesselated NURBS representation on the left: + + + +## NURBS Curve Representation + +The library uses the "Rhino" form of NURBS curves, where there are two fewer +control points than in "traditional" NURBS. \ No newline at end of file diff --git a/diagrams/circle.svg b/diagrams/circle.svg new file mode 100644 index 0000000..c9e28b3 --- /dev/null +++ b/diagrams/circle.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/diagrams/cubic-bezier.svg b/diagrams/cubic-bezier.svg new file mode 100644 index 0000000..4ebc826 --- /dev/null +++ b/diagrams/cubic-bezier.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/curve.rs b/src/curve.rs new file mode 100644 index 0000000..884f3fe --- /dev/null +++ b/src/curve.rs @@ -0,0 +1,268 @@ +use alga::general::RealField; +use nalgebra::geometry::Point3; +use nalgebra::geometry::Point4; +use nalgebra::Scalar; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(PartialEq, Debug)] +pub struct Curve { + degree: usize, + control_points: Vec>, + normalized_control_points: Vec>, + knots: Vec, +} + +impl Curve { + pub fn new(degree: usize, control_points: Vec>, knots: Vec) -> Result { + if degree == 0 { + Err(CurveError::InvalidDegree) + } else { + let required_knot_len = degree + control_points.len() - 1; + if control_points.len() <= degree { + Err(CurveError::InsufficientControlPoints { + degree, + number_supplied: control_points.len() as u16, + }) + } else if knots.len() != required_knot_len { + Err(CurveError::InvalidKnotCount { + number_received: knots.len(), + number_expected: required_knot_len, + }) + } else if !Self::knots_ordered(&knots) { + Err(CurveError::InvalidKnotOrder) + } else { + let mut normalized_control_points = Vec::new(); + for cp in &control_points { + normalized_control_points.push(Point4::new( + cp.coords.x * cp.coords.w, + cp.coords.y * cp.coords.w, + cp.coords.z * cp.coords.w, + cp.coords.w, + )) + } + Ok(Curve { + degree, + control_points, + normalized_control_points, + knots, + }) + } + } + } + + pub fn de_boor(self: &Self, u: N) -> Point3 { + let p = self.degree; + + // Construct a traditional knot vector with extra terminal knots + let mut ks = Vec::with_capacity(self.knots.len() + 2); + ks.push(self.knots[0].clone()); + ks.append(&mut self.knots.clone()); + ks.push(self.knots.last().unwrap().clone()); + + // Find minimum and maximum parameter values + let min_param = &ks[0]; + let max_param = ks.last().unwrap(); + + // clamp u to the allowed range + let mut uu = u; + if &uu < min_param { + uu = min_param.clone(); + } + if &uu > max_param { + uu = max_param.clone(); + } + + // find the index of the knot interval that contains u + // needs some special handling at the end of the parameter range + let mut k; + if &uu < max_param { + k = 0; + loop { + if ks[k] <= uu && ks[k + 1] > uu { + break; + } else { + k += 1; + } + } + } else { + k = ks.len() - 1; + while ks[k - 1] == ks[k] { + k -= 1; + } + k -= 1; + } + + // populate the initial d values + let mut d = Vec::new(); + for j in 0..(p + 1) { + d.push(self.normalized_control_points[j + k - p].clone().coords); + } + + // main de Boor algorithm + for r in 1..(p + 1) { + for j in (r..p + 1).rev() { + let alpha = (uu.clone() - ks[j + k - p].clone()) + / (ks[j + 1 + k - r].clone() - ks[j + k - p].clone()); + d[j] = d[j - 1] * (N::one() - alpha) + d[j] * alpha; + } + } + + let p4 = d[p]; + Point3::new(p4.x / p4.w, p4.y / p4.w, p4.z / p4.w) + } + + pub fn control_points(self: &Self) -> &Vec> { + &self.control_points + } + + pub fn min_u(self: &Self) -> &N { + &self.knots[0] + } + + pub fn max_u(self: &Self) -> &N { + &self.knots.last().unwrap() + } + + pub fn uniform_scale(self: &mut Self, scale_factor: N) { + for cp in &mut self.control_points { + cp.coords.x *= scale_factor; + cp.coords.y *= scale_factor; + cp.coords.z *= scale_factor + } + for cp in &mut self.normalized_control_points { + cp.coords.x *= scale_factor; + cp.coords.y *= scale_factor; + cp.coords.z *= scale_factor + } + } + + fn knots_ordered(knots: &[N]) -> bool { + let mut current_knot = &knots[0]; + for knot in &knots[1..] { + if current_knot <= knot { + current_knot = knot; + } else { + return false; + } + } + return true; + } +} + +#[derive(Error, Debug, PartialEq)] +pub enum CurveError { + #[error("invalid degree; must be > 0")] + InvalidDegree, + + #[error("insufficient control points were supplied (N={}) for a curve of \ + degree {}; at least {} are required", + .number_supplied, + .degree, + .degree - 1)] + InsufficientControlPoints { degree: usize, number_supplied: u16 }, + + #[error("expected {} knot values, but received {}", + .number_expected, + .number_received)] + InvalidKnotCount { + number_received: usize, + number_expected: usize, + }, + + #[error("knots were not supplied in nondecreasing order")] + InvalidKnotOrder, +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_relative_eq; + + #[test] + fn invalid_degree() { + let result = Curve::::new(0, vec![], vec![]); + assert_eq!(result, Err(CurveError::InvalidDegree)); + } + + #[test] + fn insufficient_control_points() { + let result = Curve::::new( + 3, + vec![ + Point4::new(-10.0, 10.0, 0.0, 1.0), + Point4::new(10.0, 10.0, 0.0, 1.0), + ], + vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + ); + assert_eq!( + result, + Err(CurveError::InsufficientControlPoints { + degree: 3, + number_supplied: 2 + }) + ); + } + + #[test] + fn invalid_knot_count() { + let result = Curve::::new( + 3, + vec![ + Point4::new(-10.0, 10.0, 0.0, 1.0), + Point4::new(10.0, 10.0, 0.0, 1.0), + Point4::new(-10.0, -10.0, 0.0, 1.0), + Point4::new(10.0, -10.0, 0.0, 1.0), + ], + vec![0.0, 0.0, 1.0, 1.0], + ); + assert_eq!( + result, + Err(CurveError::InvalidKnotCount { + number_received: 4, + number_expected: 6 + }) + ); + } + + #[test] + fn invalid_knot_order() { + let result = Curve::::new( + 3, + vec![ + Point4::new(-10.0, 10.0, 0.0, 1.0), + Point4::new(10.0, 10.0, 0.0, 1.0), + Point4::new(-10.0, -10.0, 0.0, 1.0), + Point4::new(10.0, -10.0, 0.0, 1.0), + ], + vec![1.0, 1.0, 1.0, 0.0, 0.0, 0.0], + ); + assert_eq!(result, Err(CurveError::InvalidKnotOrder)); + } + + #[test] + fn de_boor_simple() { + let test_curve = Curve::::new( + 3, + vec![ + Point4::new(-10.0, 10.0, 0.0, 1.0), + Point4::new(10.0, 10.0, 0.0, 1.0), + Point4::new(-10.0, -10.0, 0.0, 1.0), + Point4::new(10.0, -10.0, 0.0, 1.0), + ], + vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + ) + .unwrap(); + + // tests for in-range parameter + assert_relative_eq!(Point3::new(-10.0, 10.0, 0.0), test_curve.de_boor(0.0)); + assert_relative_eq!(Point3::new(-2.16, 7.92, 0.0), test_curve.de_boor(0.2)); + assert_relative_eq!(Point3::new(0.0, 0.0, 0.0), test_curve.de_boor(0.5)); + assert_relative_eq!(Point3::new(10.0, -10.0, 0.0), test_curve.de_boor(1.0)); + + // tests with parameter out-of-range (clipped to parameter range) + assert_relative_eq!(Point3::new(-10.0, 10.0, 0.0), test_curve.de_boor(-1.0)); + assert_relative_eq!(Point3::new(10.0, -10.0, 0.0), test_curve.de_boor(2.0)); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3814d5d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod curve; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7a11a9..80966b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,190 @@ +extern crate svg; + +use capstan::curve::Curve; +use nalgebra::geometry::Point4; +use svg::node::element::path; +use svg::node::element::Circle; +use svg::node::element::Group; +use svg::node::element::Path; +use svg::node::Node; +use svg::Document; + fn main() { - println!("Hello, world!"); + println!("Plotting some examples"); + + circle_example(&String::from("circle.svg")); + cubic_bezier_example(&String::from("cubic-bezier.svg")); +} + +fn circle_example(filename: &String) { + let radius = 130.0; + + let mut nurbs_circle = unit_circle(); + nurbs_circle.uniform_scale(radius); + let nurbs_group = + curve_and_control_polygon(&nurbs_circle, 256).set("transform", "translate(150, 150)"); + + let circle = style_regular(Circle::new().set("cx", 0).set("cy", 0).set("r", radius)) + .set("transform", "translate(450, 150)"); + + let document = Document::new() + .set("width", 600) + .set("height", 300) + .add(nurbs_group) + .add(circle); + + svg::save(filename, &document).unwrap(); +} + +fn cubic_bezier_example(filename: &String) { + let bezier = style_regular( + Path::new().set( + "d", + path::Data::new() + .move_to((80, 20)) + .cubic_curve_to((280, 280, 20, 280, 220, 20)), + ), + ) + .set("transform", "translate(300, 0)"); + + let nurb_bezier = curve_and_control_polygon( + &Curve::new( + 3, + vec![ + Point4::new(80.0, 20.0, 0.0, 1.0), + Point4::new(280.0, 280.0, 0.0, 1.0), + Point4::new(20.0, 280.0, 0.0, 1.0), + Point4::new(220.0, 20.0, 0.0, 1.0), + ], + vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + ) + .unwrap(), + 256, + ); + + let document = Document::new() + .set("width", 600) + .set("height", 300) + .add(nurb_bezier) + .add(bezier); + + svg::save(filename, &document).unwrap(); +} + +fn style_regular(node: T) -> Group +where + T: Node, +{ + Group::new() + .add(node) + .set("fill", "none") + .set("stroke", "blue") + .set("stroke-width", "2px") + .set("vector-effect", "non-scaling-stroke") +} + +fn curve_and_control_polygon(curve: &Curve, n_divisions: usize) -> Group { + Group::new() + .add(curve_path(curve, n_divisions)) + .add(curve_polygon(curve)) +} + +fn curve_path(curve: &Curve, n_divisions: usize) -> Path { + Path::new() + .set("d", curve_path_data(curve, n_divisions)) + .set("fill", "none") + .set("stroke", "#711081") + .set("stroke-width", "2px") + .set("vector-effect", "non-scaling-stroke") +} + +fn curve_path_data(curve: &Curve, n_divisions: usize) -> path::Data { + let min_u = curve.min_u(); + let max_u = curve.max_u(); + let u_range = max_u - min_u; + let range_denom = n_divisions as f64; + + let mut commands = Vec::with_capacity(n_divisions + 1); + commands.push(path::Command::Move( + path::Position::Absolute, + path::Parameters::from(eval_curve_2d(&curve, *min_u)), + )); + for i in 1..(n_divisions + 1) { + let u = min_u + (i as f64) * u_range / range_denom; + commands.push(path::Command::Line( + path::Position::Absolute, + path::Parameters::from(eval_curve_2d(&curve, u)), + )) + } + + path::Data::from(commands) +} + +fn curve_polygon(curve: &Curve) -> Group { + // control points + let cps = curve.control_points(); + + // a group for the control points + let mut control_points_group = Group::new(); + for cp in cps { + let cp_circle = control_point(cp.coords.x, cp.coords.y, 3.5); + control_points_group = control_points_group.add(cp_circle); + } + + // the control polygon lines + let mut commands = Vec::with_capacity(cps.len()); + commands.push(path::Command::Move( + path::Position::Absolute, + path::Parameters::from((cps[0].coords.x, cps[0].coords.y)), + )); + for i in 1..cps.len() { + commands.push(path::Command::Line( + path::Position::Absolute, + path::Parameters::from((cps[i].coords.x, cps[i].coords.y)), + )); + } + let path_data = path::Data::from(commands); + let path = Path::new() + .set("d", path_data) + .set("fill", "none") + .set("stroke", "#101010") + .set("stroke-width", "1px") + .set("stroke-dasharray", "4 3") + .set("vector-effect", "non-scaling-stroke"); + + Group::new().add(path).add(control_points_group) +} + +fn control_point(x: f64, y: f64, radius: f64) -> Circle { + Circle::new() + .set("cx", x) + .set("cy", y) + .set("r", radius) + .set("fill", "#AAAAAA") + .set("stroke", "#000000") + .set("stroke-width", "1px") + .set("vector-effect", "non-scaling-stroke") +} + +fn eval_curve_2d(curve: &Curve, u: f64) -> (f64, f64) { + let pt_3d = curve.de_boor(u).coords; + (pt_3d.x, pt_3d.y) +} + +fn unit_circle() -> Curve { + let r = f64::sqrt(2.0) / 2.0; + let degree = 2; + let control_points = vec![ + Point4::new(1.0, 0.0, 0.0, 1.0), + Point4::new(1.0, 1.0, 0.0, r), + Point4::new(0.0, 1.0, 0.0, 1.0), + Point4::new(-1.0, 1.0, 0.0, r), + Point4::new(-1.0, 0.0, 0.0, 1.0), + Point4::new(-1.0, -1.0, 0.0, r), + Point4::new(0.0, -1.0, 0.0, 1.0), + Point4::new(1.0, -1.0, 0.0, r), + Point4::new(1.0, 0.0, 0.0, 1.0), + ]; + let knots = vec![0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0]; + Curve::new(degree, control_points, knots).unwrap() }