Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions bracket-geometry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 42 additions & 0 deletions bracket-geometry/examples/bezier_curve.rs
Original file line number Diff line number Diff line change
@@ -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<char> = 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");
}
154 changes: 152 additions & 2 deletions bracket-geometry/src/curve.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::prelude::Point;
use crate::prelude::{Point, PointF};

/// Defines a two-dimensional curve by its control points.
///
Expand Down Expand Up @@ -48,6 +48,60 @@ impl Curve {
pub fn last(&self) -> Option<Point> {
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<PointF> {
if self.control_points.is_empty() || !t.is_finite() {
return None;
}

let mut points: Vec<PointF> = 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<Point> {
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<Vec<Point>> for Curve {
Expand All @@ -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() {
Expand Down Expand Up @@ -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)));
}
}
Loading