From 5b8f40cf6fe435c639e2cf873e25090cd03c8698 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 24 May 2026 12:14:21 +0900 Subject: [PATCH 1/7] feat: add bezier curve point evaluation --- bracket-geometry/src/curve.rs | 109 +++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/bracket-geometry/src/curve.rs b/bracket-geometry/src/curve.rs index 73b118c..48985a0 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,33 @@ 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]) + } } impl From> for Curve { @@ -64,7 +91,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 +141,67 @@ 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); + } + + #[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); + } } From a165bd1220c3192bb255430da596d6afee41f0a8 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 24 May 2026 12:14:56 +0900 Subject: [PATCH 2/7] feat: add bezier curve point sampling --- bracket-geometry/src/curve.rs | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/bracket-geometry/src/curve.rs b/bracket-geometry/src/curve.rs index 48985a0..498f1e6 100644 --- a/bracket-geometry/src/curve.rs +++ b/bracket-geometry/src/curve.rs @@ -75,6 +75,33 @@ impl Curve { 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 { @@ -147,6 +174,7 @@ mod tests { let curve = Curve::default(); assert_eq!(curve.bezier_point(0.5), None); + assert!(curve.bezier_points(10).is_empty()); } #[test] @@ -204,4 +232,21 @@ mod tests { 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))); + } } From d6eeedb1211e3b9ff12b2606ac2dff5d8672c3ef Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 24 May 2026 12:15:21 +0900 Subject: [PATCH 3/7] docs: document bezier curve usage --- bracket-geometry/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bracket-geometry/README.md b/bracket-geometry/README.md index c8605a0..a64807c 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. From 04ce552b4c95a6ef893f911bbe25c84afc164ad0 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 24 May 2026 12:16:07 +0900 Subject: [PATCH 4/7] docs: add bezier curve example --- bracket-geometry/examples/bezier_curve.rs | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 bracket-geometry/examples/bezier_curve.rs diff --git a/bracket-geometry/examples/bezier_curve.rs b/bracket-geometry/examples/bezier_curve.rs new file mode 100644 index 0000000..aaed70c --- /dev/null +++ b/bracket-geometry/examples/bezier_curve.rs @@ -0,0 +1,32 @@ +use bracket_geometry::prelude::*; +use crossterm::queue; +use crossterm::style::Print; +use std::io::{stdout, Write}; + +fn main() { + let curve = Curve::new(vec![Point::new(1, 8), Point::new(4, 1), Point::new(8, 8)]); + + let mut fake_console: Vec = vec!['.'; 100]; + for point in curve.bezier_points(32) { + if point.x >= 0 && point.x < 10 && point.y >= 0 && point.y < 10 { + let idx = ((point.y * 10) + point.x) as usize; + fake_console[idx] = '*'; + } + } + + for control_point in curve.control_points() { + let idx = ((control_point.y * 10) + control_point.x) as usize; + fake_console[idx] = 'o'; + } + + for y in 0..10 { + let mut line = String::from(""); + let idx = y * 10; + for x in 0..10 { + line.push(fake_console[idx + x]); + } + line.push('\n'); + queue!(stdout(), Print(&line)).expect("Command fail"); + } + stdout().flush().expect("Flush Fail"); +} From d7448e80a2aba53bd52c02fe5168c036db886bae Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 24 May 2026 12:27:05 +0900 Subject: [PATCH 5/7] docs: expand bezier curve example --- bracket-geometry/examples/bezier_curve.rs | 28 +++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/bracket-geometry/examples/bezier_curve.rs b/bracket-geometry/examples/bezier_curve.rs index aaed70c..818c19e 100644 --- a/bracket-geometry/examples/bezier_curve.rs +++ b/bracket-geometry/examples/bezier_curve.rs @@ -3,26 +3,36 @@ use crossterm::queue; use crossterm::style::Print; use std::io::{stdout, Write}; +const WIDTH: i32 = 40; +const HEIGHT: i32 = 16; + fn main() { - let curve = Curve::new(vec![Point::new(1, 8), Point::new(4, 1), Point::new(8, 8)]); + 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!['.'; 100]; - for point in curve.bezier_points(32) { - if point.x >= 0 && point.x < 10 && point.y >= 0 && point.y < 10 { - let idx = ((point.y * 10) + point.x) as usize; + 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 * 10) + control_point.x) as usize; + let idx = ((control_point.y * WIDTH) + control_point.x) as usize; fake_console[idx] = 'o'; } - for y in 0..10 { + for y in 0..HEIGHT { let mut line = String::from(""); - let idx = y * 10; - for x in 0..10 { + let idx = (y * WIDTH) as usize; + for x in 0..WIDTH as usize { line.push(fake_console[idx + x]); } line.push('\n'); From 521e8c080c8a447e1ac4f2580ddd162a6dd54962 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 24 May 2026 12:35:19 +0900 Subject: [PATCH 6/7] style: split bezier example io imports --- bracket-geometry/examples/bezier_curve.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bracket-geometry/examples/bezier_curve.rs b/bracket-geometry/examples/bezier_curve.rs index 818c19e..756d64d 100644 --- a/bracket-geometry/examples/bezier_curve.rs +++ b/bracket-geometry/examples/bezier_curve.rs @@ -1,7 +1,8 @@ use bracket_geometry::prelude::*; use crossterm::queue; use crossterm::style::Print; -use std::io::{stdout, Write}; +use std::io::stdout; +use std::io::Write; const WIDTH: i32 = 40; const HEIGHT: i32 = 16; From bd3a1f2feea46f75357dcaba9e0c9ea4c3c825d5 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 24 May 2026 12:48:21 +0900 Subject: [PATCH 7/7] style: avoid stdout import ordering --- bracket-geometry/examples/bezier_curve.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bracket-geometry/examples/bezier_curve.rs b/bracket-geometry/examples/bezier_curve.rs index 756d64d..fc37b95 100644 --- a/bracket-geometry/examples/bezier_curve.rs +++ b/bracket-geometry/examples/bezier_curve.rs @@ -1,7 +1,6 @@ use bracket_geometry::prelude::*; use crossterm::queue; use crossterm::style::Print; -use std::io::stdout; use std::io::Write; const WIDTH: i32 = 40; @@ -37,7 +36,7 @@ fn main() { line.push(fake_console[idx + x]); } line.push('\n'); - queue!(stdout(), Print(&line)).expect("Command fail"); + queue!(std::io::stdout(), Print(&line)).expect("Command fail"); } - stdout().flush().expect("Flush Fail"); + std::io::stdout().flush().expect("Flush Fail"); }