Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(canvas): compute circle coordinates with Pyth thm #917

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
132 changes: 118 additions & 14 deletions src/widgets/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
mod rectangle;
mod world;

use std::{fmt::Debug, iter::zip};
use std::{fmt::Debug, iter::zip, ops};

use itertools::Itertools;

Expand Down Expand Up @@ -405,21 +405,117 @@
/// assert_eq!(point, Some((0, 0)));
/// ```
pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
let left = self.context.x_bounds[0];
let right = self.context.x_bounds[1];
let top = self.context.y_bounds[1];
let bottom = self.context.y_bounds[0];
if x < left || x > right || y < bottom || y > top {
return None;
let x = self.get_point_x(x).ok()?;
let y = self.get_point_y(y).ok()?;
Some((x, y))
}

/// Convert the X-coordinate value from canvas coordinates to grid coordinates.
pub fn get_point_x(&self, x: f64) -> Result<usize, usize> {
self.get_point_component(x, self.context.x_bounds, self.resolution.0, false)
}

/// Convert the Y-coordinate value from canvas coordinates to grid coordinates.
///
/// Returns `Err` containing the nearest grid coordinate if it is out of bounds.
pub fn get_point_y(&self, y: f64) -> Result<usize, usize> {
self.get_point_component(y, self.context.y_bounds, self.resolution.1, true)
}

fn get_point_component(
&self,
input: f64,
[start, end]: [f64; 2],
resolution: f64,
inverted: bool,
) -> Result<usize, usize> {
let width = (end - start).abs();
if width == 0.0 {
return Err(0);
}
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
if width == 0.0 || height == 0.0 {
return None;
let scale = (resolution - 1.0) / width;

let canvas_offset = move |input| {
if inverted {
end - input
} else {
input - start
}
};

if input < start {
return Err((canvas_offset(start) * scale) as usize);
}
let x = ((x - left) * (self.resolution.0 - 1.0) / width) as usize;
let y = ((top - y) * (self.resolution.1 - 1.0) / height) as usize;
Some((x, y))
if input > end {
return Err((canvas_offset(end) * scale) as usize);
}

Ok((canvas_offset(input) * scale) as usize)
}

/// Iterates over all canvas X-coordinates within the range that map to a pixel in the output
/// grid.
pub fn step_points_x(&self, range: ops::RangeInclusive<f64>) -> impl Iterator<Item = GridStep> {
self.step_points_component(range, self.context.x_bounds, self.resolution.0, false)
}

Check warning on line 460 in src/widgets/canvas.rs

View check run for this annotation

Codecov / codecov/patch

src/widgets/canvas.rs#L458-L460

Added lines #L458 - L460 were not covered by tests

/// Iterates over all canvas Y-coordinates within the range that map to a pixel in the output
/// grid.
pub fn step_points_y(&self, range: ops::RangeInclusive<f64>) -> impl Iterator<Item = GridStep> {
self.step_points_component(range, self.context.y_bounds, self.resolution.1, true)
}

fn step_points_component(
&self,
range: ops::RangeInclusive<f64>,
[bounds_start, bounds_end]: [f64; 2],
resolution: f64,
inverted: bool,
) -> impl Iterator<Item = GridStep> {
let canvas_offset = move |input| {
if inverted {
bounds_end - input
} else {
input - bounds_start

Check warning on line 479 in src/widgets/canvas.rs

View check run for this annotation

Codecov / codecov/patch

src/widgets/canvas.rs#L479

Added line #L479 was not covered by tests
}
};

let width = (bounds_end - bounds_start).abs();
(width > 0.0)
.then(move || {
let scale = (resolution - 1.0) / width;

let start_canvas = bounds_start.max(*range.start());
let end_canvas = bounds_end.min(*range.end());

(end_canvas >= start_canvas).then(move || {
let start_grid = (canvas_offset(start_canvas) * scale) as usize;
let end_grid = (canvas_offset(end_canvas) * scale) as usize;

// non-inverted: grid = (canvas - bounds_start) * scale <=> canvas = grid /
// scale + bounds_start inverted: grid = (bounds_end -
// canvas) * scale <=> canvas = bounds_end - grid / scale

(start_grid.min(end_grid)..=start_grid.max(end_grid) + 1)
.map(move |grid_coord| {
let grid_scaled = (grid_coord as f64) / scale;
let canvas_coord = if inverted {
bounds_end - grid_scaled
} else {
bounds_start + grid_scaled

Check warning on line 505 in src/widgets/canvas.rs

View check run for this annotation

Codecov / codecov/patch

src/widgets/canvas.rs#L505

Added line #L505 was not covered by tests
};
(canvas_coord, grid_coord)
})
.tuple_windows()
.map(|(start, end)| GridStep {
canvas: start.0..end.0,
grid: start.1,
})
})
})
.flatten()
.into_iter()
.flatten()
}

/// Paint a point of the grid
Expand All @@ -438,6 +534,14 @@
}
}

/// An iterator item of [`Painter::step_points_x`]/[`Painter::step_points_y`].
pub struct GridStep {
/// The range of canvas coordinates that would draw on this grid coordinate.
pub canvas: ops::Range<f64>,
/// The grid coordinate of this step.
pub grid: usize,
}

impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> {
let resolution = context.grid.resolution();
Expand Down
48 changes: 41 additions & 7 deletions src/widgets/canvas/circle.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{convert, mem};

use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
Expand All @@ -18,12 +20,44 @@ pub struct Circle {

impl Shape for Circle {
fn draw(&self, painter: &mut Painter<'_, '_>) {
for angle in 0..360 {
let radians = f64::from(angle).to_radians();
kdheepak marked this conversation as resolved.
Show resolved Hide resolved
let circle_x = self.radius.mul_add(radians.cos(), self.x);
let circle_y = self.radius.mul_add(radians.sin(), self.y);
if let Some((x, y)) = painter.get_point(circle_x, circle_y) {
painter.paint(x, y, self.color);
fn swap_sort<T: PartialOrd>(a: &mut T, b: &mut T) {
if a > b {
mem::swap(a, b);
}
}

// draw circle line by line
for y_step in painter.step_points_y((self.y - self.radius)..=(self.y + self.radius)) {
// identify all x pixels covered on this line
let [dx_start, dx_end] = [y_step.canvas.start, y_step.canvas.end].map(|canvas_y| {
// dx_start..dx_end is the range of dx values such that dx^2 + dy^2 = r^2
// for all dy contained in y_step.canvas
let r2 = self.radius.powi(2);
let dy2 = (canvas_y - self.y).powi(2);
if r2 > dy2 {
(r2 - dy2).sqrt()
} else {
0.0 // possibly float precision error, dx should be 0 since this implies dy is
// out of the circle
}
});

for sign in [-1., 1.] {
let canvas_start = self.x + dx_start * sign;
let mut grid_start = painter
.get_point_x(canvas_start)
.unwrap_or_else(convert::identity);

let canvas_end = self.x + dx_end * sign;
let mut grid_end = painter
.get_point_x(canvas_end)
.unwrap_or_else(convert::identity);

swap_sort(&mut grid_start, &mut grid_end);

for grid_x in grid_start..=grid_end {
painter.paint(grid_x, y_step.grid, self.color);
}
}
}
}
Expand Down Expand Up @@ -60,7 +94,7 @@ mod tests {
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
" ⢀⣠⢤⣀ ",
" ⢰⠋ ⠈",
" ⢰⠋ ⠈",
" ⠘⣆⡀ ⣠⠇",
" ⠉⠉⠁ ",
" ",
Expand Down
Loading