diff --git a/bracket-geometry/README.md b/bracket-geometry/README.md index c8605a05..a64807c4 100755 --- a/bracket-geometry/README.md +++ b/bracket-geometry/README.md @@ -68,6 +68,21 @@ assert_eq!(curve.first(), Some(Point::new(0, 0))); assert_eq!(curve.last(), Some(Point::new(9, 2))); ``` +Curves can also be evaluated as Bezier curves: + +```rust +use bracket_geometry::prelude::*; +let curve = Curve::new(vec![Point::new(0, 0), Point::new(4, 8), Point::new(9, 2)]); +let midpoint = curve.bezier_point(0.5).unwrap(); +let sampled_points = curve.bezier_points(20); +``` + +You can run the Bezier curve example with: + +```sh +cargo run -p bracket-geometry --example bezier_curve +``` + ## Line Plotting Line plotting is provided using Bresenham and vector algorithms. You can return points in the line as either a vector of `Point` objects, or an iterator. diff --git a/bracket-geometry/examples/bezier_curve.rs b/bracket-geometry/examples/bezier_curve.rs new file mode 100644 index 00000000..fc37b958 --- /dev/null +++ b/bracket-geometry/examples/bezier_curve.rs @@ -0,0 +1,42 @@ +use bracket_geometry::prelude::*; +use crossterm::queue; +use crossterm::style::Print; +use std::io::Write; + +const WIDTH: i32 = 40; +const HEIGHT: i32 = 16; + +fn main() { + let curve = Curve::new(vec![ + Point::new(2, 13), + Point::new(6, 1), + Point::new(15, 4), + Point::new(23, 15), + Point::new(31, 2), + Point::new(37, 12), + ]); + + let mut fake_console: Vec = vec!['.'; (WIDTH * HEIGHT) as usize]; + for point in curve.bezier_points(160) { + if point.x >= 0 && point.x < WIDTH && point.y >= 0 && point.y < HEIGHT { + let idx = ((point.y * WIDTH) + point.x) as usize; + fake_console[idx] = '*'; + } + } + + for control_point in curve.control_points() { + let idx = ((control_point.y * WIDTH) + control_point.x) as usize; + fake_console[idx] = 'o'; + } + + for y in 0..HEIGHT { + let mut line = String::from(""); + let idx = (y * WIDTH) as usize; + for x in 0..WIDTH as usize { + line.push(fake_console[idx + x]); + } + line.push('\n'); + queue!(std::io::stdout(), Print(&line)).expect("Command fail"); + } + std::io::stdout().flush().expect("Flush Fail"); +} diff --git a/bracket-geometry/src/curve.rs b/bracket-geometry/src/curve.rs index 73b118c6..498f1e6c 100644 --- a/bracket-geometry/src/curve.rs +++ b/bracket-geometry/src/curve.rs @@ -1,4 +1,4 @@ -use crate::prelude::Point; +use crate::prelude::{Point, PointF}; /// Defines a two-dimensional curve by its control points. /// @@ -48,6 +48,60 @@ impl Curve { pub fn last(&self) -> Option { self.control_points.last().copied() } + + /// Evaluates this curve as a Bezier curve at parameter `t`. + /// + /// `t` is typically in the range `0.0..=1.0`, where `0.0` returns the + /// first control point and `1.0` returns the last control point. Values + /// outside that range are evaluated without clamping. + #[must_use] + pub fn bezier_point(&self, t: f32) -> Option { + if self.control_points.is_empty() || !t.is_finite() { + return None; + } + + let mut points: Vec = self + .control_points + .iter() + .map(|point| point.to_vec2()) + .collect(); + + while points.len() > 1 { + points = points + .windows(2) + .map(|segment| segment[0] * (1.0 - t) + segment[1] * t) + .collect(); + } + + Some(points[0]) + } + + /// Samples this curve as a Bezier curve using integer points. + /// + /// `steps` is the number of curve segments to sample. A non-empty curve + /// sampled with `steps > 0` returns `steps + 1` points. + #[must_use] + #[allow(clippy::cast_precision_loss)] + pub fn bezier_points(&self, steps: usize) -> Vec { + if self.control_points.is_empty() { + return Vec::new(); + } + + if steps == 0 { + return self + .bezier_point(0.0) + .map(Point::from_vec2) + .into_iter() + .collect(); + } + + (0..=steps) + .filter_map(|i| { + let t = i as f32 / steps as f32; + self.bezier_point(t).map(Point::from_vec2) + }) + .collect() + } } impl From> for Curve { @@ -64,7 +118,22 @@ impl From<&[Point]> for Curve { #[cfg(test)] mod tests { - use crate::prelude::{Curve, Point}; + use crate::prelude::{Curve, Point, PointF}; + + fn assert_pointf_eq(actual: PointF, expected_x: f32, expected_y: f32) { + const EPSILON: f32 = 0.001; + + assert!( + (actual.x - expected_x).abs() < EPSILON, + "expected x to be {expected_x}, got {}", + actual.x + ); + assert!( + (actual.y - expected_y).abs() < EPSILON, + "expected y to be {expected_y}, got {}", + actual.y + ); + } #[test] fn new_curve_stores_control_points() { @@ -99,4 +168,85 @@ mod tests { assert_eq!(curve.control_points(), &points); } + + #[test] + fn empty_curve_has_no_bezier_points() { + let curve = Curve::default(); + + assert_eq!(curve.bezier_point(0.5), None); + assert!(curve.bezier_points(10).is_empty()); + } + + #[test] + fn single_control_point_bezier_returns_that_point() { + let curve = Curve::new(vec![Point::new(3, 7)]); + + assert_pointf_eq(curve.bezier_point(0.0).unwrap(), 3.0, 7.0); + assert_pointf_eq(curve.bezier_point(0.5).unwrap(), 3.0, 7.0); + assert_pointf_eq(curve.bezier_point(1.0).unwrap(), 3.0, 7.0); + } + + #[test] + fn linear_bezier_interpolates_between_two_points() { + let curve = Curve::new(vec![Point::new(0, 0), Point::new(10, 10)]); + + assert_pointf_eq(curve.bezier_point(0.5).unwrap(), 5.0, 5.0); + } + + #[test] + fn quadratic_bezier_evaluates_midpoint() { + let curve = Curve::new(vec![ + Point::new(0, 0), + Point::new(10, 10), + Point::new(20, 0), + ]); + + assert_pointf_eq(curve.bezier_point(0.5).unwrap(), 10.0, 5.0); + } + + #[test] + fn cubic_bezier_evaluates_midpoint() { + let curve = Curve::new(vec![ + Point::new(0, 0), + Point::new(0, 10), + Point::new(10, 10), + Point::new(10, 0), + ]); + + assert_pointf_eq(curve.bezier_point(0.5).unwrap(), 5.0, 7.5); + } + + #[test] + fn bezier_endpoints_match_first_and_last_control_points() { + let curve = Curve::new(vec![Point::new(1, 2), Point::new(5, 8), Point::new(9, 3)]); + + assert_pointf_eq(curve.bezier_point(0.0).unwrap(), 1.0, 2.0); + assert_pointf_eq(curve.bezier_point(1.0).unwrap(), 9.0, 3.0); + } + + #[test] + fn non_finite_bezier_parameter_returns_none() { + let curve = Curve::new(vec![Point::new(0, 0), Point::new(10, 10)]); + + assert_eq!(curve.bezier_point(f32::NAN), None); + assert_eq!(curve.bezier_point(f32::INFINITY), None); + assert_eq!(curve.bezier_point(f32::NEG_INFINITY), None); + } + + #[test] + fn zero_step_bezier_sampling_returns_start_point() { + let curve = Curve::new(vec![Point::new(2, 3), Point::new(6, 9)]); + + assert_eq!(curve.bezier_points(0), vec![Point::new(2, 3)]); + } + + #[test] + fn bezier_sampling_returns_steps_plus_one_points() { + let curve = Curve::new(vec![Point::new(0, 0), Point::new(10, 10)]); + let points = curve.bezier_points(4); + + assert_eq!(points.len(), 5); + assert_eq!(points.first(), Some(&Point::new(0, 0))); + assert_eq!(points.last(), Some(&Point::new(10, 10))); + } }