diff --git a/Cargo.toml b/Cargo.toml index 63888fa..e8d0579 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,4 @@ members = [ ] [patch.crates-io] -embedded-graphics = { git = "https://github.com/embedded-graphics/embedded-graphics.git"} -embedded-graphics-simulator = { git = "https://github.com/embedded-graphics/simulator.git"} +embedded-graphics = { path = "../embedded-graphics" } diff --git a/debug-tools/Cargo.toml b/debug-tools/Cargo.toml index 18aae25..d6528ad 100644 --- a/debug-tools/Cargo.toml +++ b/debug-tools/Cargo.toml @@ -7,5 +7,6 @@ publish = false [dependencies] framework = { path = "../framework" } -embedded-graphics = "0.7.0-beta.2" -embedded-graphics-simulator = "0.3.0-alpha.2" +embedded-graphics = { version = "0.8.1", path = "../../embedded-graphics" } +embedded-graphics-simulator = { version = "0.6.0", path = "../../simulator" } +integer-sqrt = "0.1.5" diff --git a/debug-tools/examples/aa-bres-float.rs b/debug-tools/examples/aa-bres-float.rs new file mode 100644 index 0000000..bf40506 --- /dev/null +++ b/debug-tools/examples/aa-bres-float.rs @@ -0,0 +1,136 @@ +//! Render a 1px wide antialiased line using error components and a 255 multiplier. +//! +//! Inspiration from + +use core::convert::TryFrom; +use embedded_graphics::{ + mock_display::MockDisplay, pixelcolor::Rgb888, prelude::*, primitives::Line, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; + +fn thick_line( + display: &mut impl DrawTarget, + line: Line, + _width: i32, +) -> Result<(), std::convert::Infallible> { + let skele_color = Rgb888::MAGENTA; + + let delta = line.delta(); + + let dx = delta.x; + let dy = delta.y; + + let mut point = line.start; + let mut error: f32 = 0.0; + let slope = dy as f32 / dx as f32; + + println!("---"); + + for _i in 0..=dx { + // // AA point above + let bright = ((0.5 - error).abs() * 256.0) as u32; + let c = Rgb888::new( + ((bright * skele_color.r() as u32) / 255) as u8, + ((bright * skele_color.g() as u32) / 255) as u8, + ((bright * skele_color.b() as u32) / 255) as u8, + ); + Pixel(Point::new(point.x, point.y - 1), c).draw(display)?; + + // // AA point below + // let bright = (255 - br) as u32; + // let c = Rgb888::new( + // ((bright * skele_color.r() as u32) / 255) as u8, + // ((bright * skele_color.g() as u32) / 255) as u8, + // ((bright * skele_color.b() as u32) / 255) as u8, + // ); + // Pixel(Point::new(point.x, point.y + 1), c).draw(display)?; + + // Line skeleton + let bright = 255; + let c = Rgb888::new( + ((bright * skele_color.r() as u32) / 255) as u8, + ((bright * skele_color.g() as u32) / 255) as u8, + ((bright * skele_color.b() as u32) / 255) as u8, + ); + Pixel(Point::new(point.x, point.y), c).draw(display)?; + + dbg!(error); + + if error > 0.5 { + point.y += 1; + error -= 1.0; + } + + error += slope; + point.x += 1; + } + + Ok(()) +} + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(200, 200); + // const DISPLAY_SIZE: Size = Size::new(64, 64); + + fn new() -> Self { + let end = Point::new( + Self::DISPLAY_SIZE.width as i32 / 2, + Self::DISPLAY_SIZE.height as i32 / 2, + ); + Self { + start: end + Point::new(10, 15), + end, + // end: start + Point::new(100, 0), + stroke_width: 10, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let Point { x: _x0, y: _y0 } = self.start; + + // let width = 2 * self.stroke_width as i32 * f32::sqrt((dx * dx + dy * dy) as f32) as i32; + // let width = (self.stroke_width as i32).pow(2) * (dx * dx + dy * dy); + let width = self.stroke_width as i32; + + let _mock_display: MockDisplay = MockDisplay::new(); + + thick_line(display, Line::new(self.start, self.end), width)?; + + // let l = Line::new(self.start, self.end); + + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + // l.perpendicular() + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(5).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/debug-tools/examples/aa-bres.rs b/debug-tools/examples/aa-bres.rs new file mode 100644 index 0000000..efb2cfa --- /dev/null +++ b/debug-tools/examples/aa-bres.rs @@ -0,0 +1,160 @@ +//! Render a 1px wide antialiased line using error components and a 255 multiplier. +//! +//! Inspiration from + +use core::convert::TryFrom; +use embedded_graphics::{ + mock_display::MockDisplay, pixelcolor::Rgb888, prelude::*, primitives::Line, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; + +fn thick_line( + display: &mut impl DrawTarget, + line: Line, + _width: i32, +) -> Result<(), std::convert::Infallible> { + let skele_color = Rgb888::MAGENTA; + + let delta = line.delta(); + + let dx = delta.x; + let dy = delta.y; + + let num = dy * 255; + let denom = dx; + // Rounding integer division + let slope = ((num) + (denom) / 2) / (denom); + + let slope = if let Ok(slope) = u8::try_from(slope) { + slope + } else { + // Most likely cause: gradient is incorrect due to improper swapping of major/minor + // direction. The slope should always be 1.0 or less, or because we multiply by 255 in this + // case, 255 or less. + return Ok(()); + }; + + let mut point = line.start; + + let mut error: i32 = 0; + + let threshold = dx - 2 * dy; + // E_diag + let e_minor = -2 * dx; + // E_square + let e_major = 2 * dy; + + // TODO: Calculate initial brightness + let mut br: u8 = 255; + dbg!(slope); + + for _i in 0..=dx { + // println!("{} {}", br, error); + // AA point above + let bright = br as u32; + let c = Rgb888::new( + ((bright * skele_color.r() as u32) / 255) as u8, + ((bright * skele_color.g() as u32) / 255) as u8, + ((bright * skele_color.b() as u32) / 255) as u8, + ); + Pixel(Point::new(point.x, point.y - 1), c).draw(display)?; + + // // AA point below + // let bright = (255 - br) as u32; + // let c = Rgb888::new( + // ((bright * skele_color.r() as u32) / 255) as u8, + // ((bright * skele_color.g() as u32) / 255) as u8, + // ((bright * skele_color.b() as u32) / 255) as u8, + // ); + // Pixel(Point::new(point.x, point.y + 1), c).draw(display)?; + + // Line skeleton + // let bright = (e * 255.0) as u32; + let bright = 255; + let c = Rgb888::new( + ((bright * skele_color.r() as u32) / 255) as u8, + ((bright * skele_color.g() as u32) / 255) as u8, + ((bright * skele_color.b() as u32) / 255) as u8, + ); + Pixel(Point::new(point.x, point.y), c).draw(display)?; + + if error > threshold { + point.y += 1; + error += e_minor; + br = 255; + } + + error += e_major; + point.x += 1; + br = br.saturating_sub(slope); + } + + Ok(()) +} + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(200, 200); + // const DISPLAY_SIZE: Size = Size::new(64, 64); + + fn new() -> Self { + let end = Point::new( + Self::DISPLAY_SIZE.width as i32 / 2, + Self::DISPLAY_SIZE.height as i32 / 2, + ); + Self { + start: end + Point::new(10, 15), + end, + // end: start + Point::new(100, 0), + stroke_width: 10, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let Point { x: _x0, y: _y0 } = self.start; + + // let width = 2 * self.stroke_width as i32 * f32::sqrt((dx * dx + dy * dy) as f32) as i32; + // let width = (self.stroke_width as i32).pow(2) * (dx * dx + dy * dy); + let width = self.stroke_width as i32; + + let _mock_display: MockDisplay = MockDisplay::new(); + + thick_line(display, Line::new(self.start, self.end), width)?; + + // let l = Line::new(self.start, self.end); + + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + // l.perpendicular() + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(5).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/debug-tools/examples/aa-wu.rs b/debug-tools/examples/aa-wu.rs new file mode 100644 index 0000000..2ccb31d --- /dev/null +++ b/debug-tools/examples/aa-wu.rs @@ -0,0 +1,245 @@ +use embedded_graphics::{ + geometry::PointExt, + mock_display::MockDisplay, + pixelcolor::Rgb888, + prelude::*, + primitives::{ + common::{LineSide, LinearEquation}, + line::StrokeOffset, + Line, PrimitiveStyle, + }, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; +use integer_sqrt::IntegerSquareRoot; + +#[derive(Debug, Copy, Clone, PartialEq)] +enum LineOffset { + Left, + Center, + Right, +} + +impl LineOffset { + fn widths(self, width: i32) -> (i32, i32) { + match width { + width => { + match self { + Self::Left => (width.saturating_sub(1), 0), + Self::Center => { + let width = width.saturating_sub(1); + + // Right-side bias for even width lines. Move mod2 to first item in the + // tuple to bias to the left instead. + (width / 2, width / 2 + (width % 2)) + } + Self::Right => (width.saturating_sub(1), 0), + } + } + } + } +} + +#[derive(Debug, Clone, Copy)] +struct MajorMinor { + major: T, + minor: T, +} + +impl MajorMinor { + fn new(major: T, minor: T) -> Self { + Self { major, minor } + } +} + +fn floor(x: f32) -> f32 { + x.floor() +} +fn round(x: f32) -> f32 { + x.round() +} + +fn fract(x: f32) -> f32 { + x.fract() +} + +fn recip_fract(x: f32) -> f32 { + 1.0 - x.fract() +} + +fn thickline( + display: &mut impl DrawTarget, + line: Line, + width: i32, +) -> Result<(), std::convert::Infallible> { + let Line { start, end } = line; + + let extents = line.extents(width as u32, StrokeOffset::None); + + let (delta, step, pstep) = { + let delta = end - start; + + let direction = Point::new( + if delta.x >= 0 { 1 } else { -1 }, + if delta.y >= 0 { 1 } else { -1 }, + ); + + let perp_direction = { + // let perp_delta = Point::new(delta.y, -delta.x); + let perp_delta = line.perpendicular(); + let perp_delta = perp_delta.end - perp_delta.start; + + Point::new( + if perp_delta.x >= 0 { 1 } else { -1 }, + if perp_delta.y >= 0 { 1 } else { -1 }, + ) + }; + + // Determine major and minor directions. + if delta.y.abs() >= delta.x.abs() { + ( + MajorMinor::new(delta.y, delta.x), + MajorMinor::new(direction.y_axis(), direction.x_axis()), + MajorMinor::new(perp_direction.y_axis(), perp_direction.x_axis()), + ) + } else { + ( + MajorMinor::new(delta.x, delta.y), + MajorMinor::new(direction.x_axis(), direction.y_axis()), + MajorMinor::new(perp_direction.x_axis(), perp_direction.y_axis()), + ) + } + }; + + let mut point = start; + let mut p = start; + + let dx = delta.major.abs(); + let dy = delta.minor.abs(); + + let threshold = dx - 2 * dy; + let e_minor = -2 * dx; + let e_major = 2 * dy; + let length = dx + 1; + let mut error = 0i32; + + let skele_color = Rgb888::MAGENTA; + let mut slope = dy as f32 / dx as f32; + + let mut e = 0.0f32; + println!("==="); + + for _i in 0..length { + println!("---"); + + // NOTE: Numerical inaccuracies cause fireflies when using `error as f32 / (2 * dx) as f32` + // (although is yields mostly the same result) from Bresenham. + let bright = if slope >= 1.0 { + // Half brightness pixels along edge when line is at 45ยบ + 255 / 2 + } else { + ((1.0 - e.fract()) * 255.0) as u32 + }; + + let delta = Point::new( + e.floor() as i32 * dx.signum(), + e.floor() as i32 * dy.signum(), + ); + let delta = delta.component_mul(step.minor); + + let c = Rgb888::new( + ((bright * skele_color.r() as u32) / 255) as u8, + ((bright * skele_color.g() as u32) / 255) as u8, + ((bright * skele_color.b() as u32) / 255) as u8, + ); + + // Wu AA pixel + Pixel(p + delta - step.minor, c).draw(display)?; + // Wu normal pixel + Pixel(p + delta, skele_color).draw(display)?; + + // Bresenham pixel + Pixel(point - Point::new(0, 4), Rgb888::WHITE).draw(display)?; + + if error > threshold { + // if 1.0 - e.fract() < slope { + point += step.minor; + error += e_minor; + println!("..."); + } + + e += slope; + error += e_major; + point += step.major; + p += step.major; + } + + Ok(()) +} + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(200, 200); + // const DISPLAY_SIZE: Size = Size::new(64, 64); + + fn new() -> Self { + let end = Point::new( + Self::DISPLAY_SIZE.width as i32 / 2, + Self::DISPLAY_SIZE.height as i32 / 2, + ); + Self { + // start: end + Point::new(10, 15), + start: Point::new(95, 99), + end, + // end: start + Point::new(100, 0), + stroke_width: 10, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let Point { x: _x0, y: _y0 } = self.start; + + // let width = 2 * self.stroke_width as i32 * f32::sqrt((dx * dx + dy * dy) as f32) as i32; + // let width = (self.stroke_width as i32).pow(2) * (dx * dx + dy * dy); + let width = self.stroke_width as i32; + + let _mock_display: MockDisplay = MockDisplay::new(); + + thickline(display, Line::new(self.start, self.end), width)?; + + // let l = Line::new(self.start, self.end); + + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + // l.perpendicular() + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(5).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/debug-tools/examples/circle-distance.rs b/debug-tools/examples/circle-distance.rs new file mode 100644 index 0000000..04e7296 --- /dev/null +++ b/debug-tools/examples/circle-distance.rs @@ -0,0 +1,125 @@ +use embedded_graphics::{ + pixelcolor::{Gray4, Gray8, Rgb888}, + prelude::*, + primitives::{Circle, PrimitiveStyleBuilder}, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::{draw, prelude::*}; + +struct CircleDebug { + center: Point, + diameter: u32, + stroke_width: u32, + show_bounding_box: bool, +} + +impl App for CircleDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(128, 128); + + fn new() -> Self { + Self { + center: Point::new(64, 80), + diameter: 70, + stroke_width: 1, + show_bounding_box: false, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("center", &mut self.center), + Parameter::new("diameter", &mut self.diameter), + Parameter::new("stroke", &mut self.stroke_width), + Parameter::new("show BB", &mut self.show_bounding_box), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let circle = Circle::with_center(self.center, self.diameter); + + let bb = circle.bounding_box(); + let center = bb.center(); + + let max_distance = bb.size.width as f32 * 2.0f32.sqrt(); + + for point in bb.points() { + let distance_to_center = + f32::sqrt(((point.x - center.x).pow(2) + (point.y - center.y).pow(2)) as f32); + + let distance_to_edge = distance_to_center - circle.diameter as f32 / 2.0; + + let norm = distance_to_center / max_distance; + + let scaled = (norm * 255.0) as u8; + + // Distance field + { + let grey = Gray8::new(scaled); + + let color = Rgb888::from(grey); + + Pixel(point, color).draw(display)?; + } + + // // Non antialiased circle + // { + // let color = if distance_to_edge < 0.0 { + // Rgb888::RED + // } else { + // Rgb888::GREEN + // }; + + // Pixel(point, color).draw(display)?; + // } + + // Antialiased circle + { + let color = (0.5 - distance_to_edge).clamp(0.0, 1.0); + + let scaled = (color * 255.0) as u8; + + // Kludge for "transparent" pixels + if scaled == 0 { + continue; + } + + let color = Rgb888::new(scaled, 0, 0); + + Pixel(point, color).draw(display)?; + } + } + + // let style = PrimitiveStyleBuilder::new() + // .stroke_color(Rgb888::CSS_SPRING_GREEN) + // .stroke_width(self.stroke_width) + // .fill_color(Rgb888::CSS_DARK_SEA_GREEN) + // .build(); + // let styled_circle = circle.into_styled(style); + + // if self.show_bounding_box { + // draw::bounding_box(&styled_circle, display); + // } + + // styled_circle.draw(display)?; + + // if self.show_bounding_box { + // draw::point(self.center, Rgb888::CSS_LIGHT_SKY_BLUE, display); + // } + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new() + .scale(3) + .pixel_spacing(1) + .build(); + let window = Window::new("Circle debugger", &settings); + + CircleDebug::run(window); +} diff --git a/debug-tools/examples/line-distance.rs b/debug-tools/examples/line-distance.rs new file mode 100644 index 0000000..fae00e6 --- /dev/null +++ b/debug-tools/examples/line-distance.rs @@ -0,0 +1,191 @@ +use embedded_graphics::{ + pixelcolor::{Gray8, Rgb888}, + prelude::*, + primitives::{ + line::{Scanline, StrokeOffset}, + Line, PrimitiveStyle, + }, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, +} + +fn distance(line: Line, point: Point) -> f32 { + let length = { + let Point { x, y } = line.delta(); + + f32::sqrt((x.pow(2) + y.pow(2)) as f32) + }; + + let x1 = line.start.x; + let x2 = line.end.x; + let x3 = point.x; + + let y1 = line.start.y; + let y2 = line.end.y; + let y3 = point.y; + + let u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) as f32 / length.powi(2); + + let tx = x1 as f32 + u * (x2 - x1) as f32; + let ty = y1 as f32 + u * (y2 - y1) as f32; + + // Tangent intersection point + let tangent = Point::new(tx as i32, ty as i32); + + let Point { x, y } = Line::new(point, tangent).delta(); + + f32::sqrt((x.pow(2) + y.pow(2)) as f32) +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(256, 256); + + fn new() -> Self { + Self { + start: Point::new(128, 128), + end: Point::new(150, 170), + stroke_width: 15, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let skeleton = Line::new(self.start, self.end); + let (l, r) = skeleton.extents(self.stroke_width, StrokeOffset::None); + + let bb = skeleton + .into_styled(PrimitiveStyle::with_stroke( + Rgb888::BLACK, + self.stroke_width, + )) + .bounding_box(); + let br = bb.bottom_right().unwrap(); + + let max_distance = bb.size.width.max(bb.size.height) as f32 * 2.0f32.sqrt(); + + // 4 lines that construct the perimiter of the thick line + let perimiter = [l, Line::new(l.end, r.end), r, Line::new(r.start, l.start)]; + + // Draw perimiter + for line in &perimiter { + line.into_styled(PrimitiveStyle::with_stroke(Rgb888::MAGENTA, 1)) + .draw(display)?; + } + + for y in bb.top_left.y..=br.y { + // --- + + // // Scanline (integer) intersection + // { + // let mut min_x = i32::MAX; + // let mut max_x = i32::MIN; + + // for line in &perimiter { + // let mut scanline = Scanline::new_empty(y); + // scanline.bresenham_intersection(&line); + + // if scanline.is_empty() { + // continue; + // } + + // min_x = min_x.min(scanline.x.start); + // max_x = max_x.max(scanline.x.end); + // } + + // let scanline = Scanline::new(y, min_x..max_x); + // scanline.draw(display, Rgb888::YELLOW)?; + // } + + // --- + + // // Distance field + // { + // let scanline = Line::new(Point::new(bb.top_left.x, y), Point::new(br.x, y)); + + // for point in scanline.points() { + // let distance = distance(skeleton, point); + + // let norm = distance / max_distance; + + // let color = Gray8::new(unsafe { (norm * 255.0).to_int_unchecked() }); + + // let color = Rgb888::from(color); + + // Pixel(point, color).draw(display)?; + // } + // } + } + + // Crappy distance function + // for point in bb.points() { + // // http://paulbourke.net/geometry/pointlineplane + // // Distance between skeleton and current point + // let distance = { + // let x1 = skeleton.start.x; + // let x2 = skeleton.end.x; + // let x3 = point.x; + + // let y1 = skeleton.start.y; + // let y2 = skeleton.end.y; + // let y3 = point.y; + + // let u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) as f32 / length.powi(2); + + // let tx = x1 as f32 + u * (x2 - x1) as f32; + // let ty = y1 as f32 + u * (y2 - y1) as f32; + + // // Tangent intersection point + // let tangent = Point::new(tx as i32, ty as i32); + + // let Point { x, y } = Line::new(point, tangent).delta(); + + // f32::sqrt((x.pow(2) + y.pow(2)) as f32) + // }; + + // let distance_to_edge = distance - self.stroke_width as f32; + + // let color = (0.5 - distance_to_edge).clamp(0.0, 1.0); + + // let color = Rgb888::from(Gray8::new(unsafe { (color * 255.0).to_int_unchecked() })); + + // Pixel(point, color).draw(display)?; + // } + + // // Structure + // { + // skeleton + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(display)?; + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, 1)) + // .draw(display)?; + // r.into_styled(PrimitiveStyle::with_stroke(Rgb888::YELLOW, 1)) + // .draw(display)?; + // } + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(3).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/debug-tools/examples/line-intersection.rs b/debug-tools/examples/line-intersection.rs index 4a5a8bd..466e6f0 100644 --- a/debug-tools/examples/line-intersection.rs +++ b/debug-tools/examples/line-intersection.rs @@ -1,6 +1,6 @@ use embedded_graphics::{ geometry::AnchorPoint, - mono_font::{latin1::FONT_6X10, MonoTextStyle}, + mono_font::{ascii::FONT_6X10, MonoTextStyle}, pixelcolor::{Rgb565, WebColors}, prelude::*, primitives::Line, diff --git a/debug-tools/examples/line-no-aa-parallel.rs b/debug-tools/examples/line-no-aa-parallel.rs new file mode 100644 index 0000000..0a480f7 --- /dev/null +++ b/debug-tools/examples/line-no-aa-parallel.rs @@ -0,0 +1,560 @@ +use embedded_graphics::{ + geometry::PointExt, + mock_display::MockDisplay, + pixelcolor::Rgb888, + prelude::*, + primitives::{Line, PrimitiveStyle}, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; + +#[derive(Debug, Copy, Clone, PartialEq)] +enum LineOffset { + Left, + Center, + Right, +} + +impl LineOffset { + fn widths(self, width: i32) -> (i32, i32) { + match width { + width => { + match self { + Self::Left => (width.saturating_sub(1), 0), + Self::Center => { + let width = width.saturating_sub(1); + + // Right-side bias for even width lines. Move mod2 to first item in the + // tuple to bias to the left instead. + (width / 2, width / 2 + (width % 2)) + } + Self::Right => (width.saturating_sub(1), 0), + } + } + } + } +} + +#[derive(Debug, Clone, Copy)] +struct MajorMinor { + major: T, + minor: T, +} + +impl MajorMinor { + fn new(major: T, minor: T) -> Self { + Self { major, minor } + } +} + +fn thickline( + display: &mut impl DrawTarget, + mut line: Line, + width: i32, + toggle: bool, + toggle2: bool, + last_offset: i32, +) -> Result<(), std::convert::Infallible> { + if width == 0 { + return Ok(()); + } + + let non_mul_line = line; + + let seed_line = line.perpendicular(); + + let mut point_left = seed_line.start; + let mut point_right = seed_line.start; + + line.start.y *= 256; + line.end.y *= 256; + + let parallel_delta = line.delta(); + let parallel_step = Point::new( + if parallel_delta.x >= 0 { 1 } else { -1 }, + if parallel_delta.y >= 0 { 1 } else { -1 }, + ); + + let seed_line_delta = seed_line.delta(); + + let seed_line_direction = Point::new( + if seed_line_delta.x >= 0 { 1 } else { -1 }, + if seed_line_delta.y >= 0 { 1 } else { -1 }, + ); + + let (seed_line_delta, seed_line_step) = if seed_line_delta.y.abs() >= seed_line_delta.x.abs() { + ( + MajorMinor::new(seed_line_delta.y, seed_line_delta.x), + MajorMinor::new(seed_line_direction.y_axis(), seed_line_direction.x_axis()), + ) + } else { + ( + MajorMinor::new(seed_line_delta.x, seed_line_delta.y), + MajorMinor::new(seed_line_direction.x_axis(), seed_line_direction.y_axis()), + ) + }; + + let (parallel_delta, parallel_step) = if parallel_delta.y.abs() >= parallel_delta.x.abs() { + ( + MajorMinor::new(parallel_delta.y, parallel_delta.x), + MajorMinor::new(parallel_step.y_axis(), parallel_step.x_axis()), + ) + } else { + ( + MajorMinor::new(parallel_delta.x, parallel_delta.y), + MajorMinor::new(parallel_step.x_axis(), parallel_step.y_axis()), + ) + }; + + // Don't draw line skeleton twice + point_right -= seed_line_step.major; + + let dx = seed_line_delta.major.abs(); + let dy = seed_line_delta.minor.abs(); + + let threshold = dx - 2 * dy; + let e_minor = -2 * dx; + let e_major = 2 * dy; + let mut seed_line_error = 0; + let mut seed_line_error_right = e_major; + // Perpendicular error or "phase" + let mut parallel_error = 0; + let mut parallel_error_right = 0; + + // This fixes the phasing for parallel lines on the left side of the base line for the octants + // where the line perpendicular moves "away" from the line body. + let original_flip = if seed_line_step.minor == -parallel_step.major { + -1 + } else { + 1 + }; + + // Subtract 1 if using AA so 1px wide lines are _only_ drawn with AA - no solid fill + let thickness_threshold = ((width - 1) * 2).pow(2) * non_mul_line.delta().length_squared(); + // Add the first line drawn to the thickness. If this is left at zero, an extra line will be + // drawn as the lines are drawn before checking for thickness. + let mut thickness_accumulator = 2 * dx; + + // Bias to one side of the line + // TODO: The current extents() function needs to respect this too, as well as stroke offset + let mut is_right = true; + + let mut right_side_aa_done = false; + let mut left_side_aa_done = false; + + while thickness_accumulator.pow(2) <= thickness_threshold { + let (point, inc, c, seed_line_error, parallel_error, flip) = if is_right { + ( + &mut point_right, + MajorMinor::new(-seed_line_step.major, -seed_line_step.minor), + // Rgb888::CSS_DARK_GOLDENROD, + Rgb888::CSS_SALMON, + &mut seed_line_error_right, + &mut parallel_error_right, + // Fix phasing for parallel lines on the right hand side of the base line + -original_flip, + ) + } else { + ( + &mut point_left, + seed_line_step, + Rgb888::CSS_FOREST_GREEN, + &mut seed_line_error, + &mut parallel_error, + original_flip, + ) + }; + + Pixel(Point::new(point.x, point.y), c).draw(display)?; + + parallel_line( + *point, + line, + parallel_step, + parallel_delta, + *parallel_error * flip, + c, + false, + last_offset, + display, + )?; + + if *seed_line_error > threshold { + *point += inc.minor; + *seed_line_error += e_minor; + thickness_accumulator += 2 * dy; + + if *parallel_error > threshold { + if thickness_accumulator.pow(2) <= thickness_threshold { + // Pixel(*point, Rgb888::CYAN).draw(display)?; + + // parallel_line( + // *point, + // line, + // parallel_step, + // parallel_delta, + // (*parallel_error + e_minor + e_major) * flip, + // // Rgb888::CYAN, + // c, + // // If we're on the side of the base line where the perpendicular + // // Bresenham steps "into" the thick line body, skip the first extra + // // line point as it's on the wrong side of the perpendicular and leads + // // to a jagged edge. + // original_flip == -1 && !is_right || original_flip == 1 && is_right, + // if original_flip == -1 && !is_right || original_flip == 1 && is_right { + // 0 + // } else { + // // Because the opposite side's extra lines start one step into the thick + // // line body, we must reduce its total length by 1 to prevent jagged + // // edges on the end edge of the line. + // -1 + // } + last_offset, + // display, + // )?; + } + + // We're currently drawing an "extra" line. Special case: if the next step would be + // line edge, draw an extra AA line and mark it as not needing to be drawn after + // the main loop. + if thickness_accumulator.pow(2) + 2 * dy > thickness_threshold { + // if is_right { + // right_side_aa_done = true; + // println!("Right side AA inside loop"); + // } else { + // left_side_aa_done = true; + // println!("Left side AA inside loop"); + // } + + // if toggle { + // parallel_line_aa( + // *point, + // line, + // parallel_step, + // (*parallel_error + e_minor + e_major) * flip, + // c, + // original_flip == -1 && !is_right || original_flip == 1 && is_right, + // true, + // if original_flip == -1 && !is_right || original_flip == 1 && is_right { + // 0 + // } else { + // // Because the opposite side's extra lines start one step into the thick + // // line body, we must reduce its total length by 1 to prevent jagged + // // edges on the end edge of the line. + // -1 + // } + last_offset, + // display, + // )?; + // } + + // Makes right-side AA line hug the rest of the line body by undoing the + // position increment at the end of the main loop. + *point -= inc.major; + } + + *parallel_error += e_minor; + } + + *parallel_error += e_major; + } + + *point += inc.major; + *seed_line_error += e_major; + thickness_accumulator += 2 * dx; + + is_right = !is_right; + } + + let flip = original_flip; + + if toggle { + if !left_side_aa_done { + parallel_line_aa( + point_left, + line, + parallel_step, + parallel_delta, + parallel_error * flip, + Rgb888::CSS_ALICE_BLUE, + false, + flip == -1, + last_offset, + display, + )?; + } + + // if !right_side_aa_done { + // parallel_line_aa( + // point_right, + // line, + // parallel_step, + // parallel_error_right * -flip, + // Rgb888::CSS_SALMON, + // false, + // flip == 1, + // last_offset, + // display, + // )?; + // } + } + + Ok(()) +} + +fn parallel_line_aa( + start: Point, + line: Line, + step: MajorMinor, + delta: MajorMinor, + start_error: i32, + c: Rgb888, + skip_first: bool, + invert: bool, + mut last_offset: i32, + display: &mut impl DrawTarget, +) -> Result<(), std::convert::Infallible> { + let mut point = Point::new(start.x, start.y * 256); + + // Pixel(point, c).draw(display)?; + // return Ok(()); + + // // https://computergraphics.stackexchange.com/a/10675 + // let step = MajorMinor::new(step.major, step.minor); + // let delta = MajorMinor::new(delta.major, delta.minor); + + let dx = delta.major.abs(); + let dy = delta.minor.abs(); + + let threshold = dx - 2 * dy; + let e_minor = -2 * dx; + let e_major = 2 * dy; + let mut length = dx + 1; + let mut error = start_error; + + if skip_first { + // Some of the length was consumed by this initial skip iteration. If this is omitted, the + // line will be drawn 1px too long. + last_offset -= 1; + + if error > threshold { + point += step.minor; + error += e_minor; + } + + error += e_major; + point += step.major; + } + + for _i in 0..(length + last_offset) { + // https://computergraphics.stackexchange.com/a/10675 + let draw_p = Point::new(point.x, (point.y >> 8) - (line.delta().y).signum()); + + Pixel(draw_p, Rgb888::CYAN).draw(display)?; + + let aa_colour = { + let c = Rgb888::RED; + + let mul = (point.y & 255) as u8; + + Rgb888::new( + // TODO: Proper colour blend + // (c.r() as f32 * (1.0 - mul as f32 / 255.0)) as u8, + // (c.g() as f32 * (1.0 - mul as f32 / 255.0)) as u8, + // (c.b() as f32 * (1.0 - mul as f32 / 255.0)) as u8, + 255 - mul, + 255 - mul, + 255 - mul, + ) + }; + + let aa_p = Point::new(point.x, (point.y >> 8) - (line.delta().y).signum() * 2); + + Pixel(aa_p, aa_colour).draw(display)?; + + // Doesn't work: mathematical distance from ideal line using line_point_distance(). Not + // quite sure why but we don't get a smooth increase over the length of the line. + + if error > threshold { + point += step.minor; + error += e_minor; + } + + error += e_major; + point += step.major; + } + + Ok(()) +} + +fn parallel_line( + start: Point, + line: Line, + step: MajorMinor, + delta: MajorMinor, + start_error: i32, + c: Rgb888, + skip_first: bool, + mut last_offset: i32, + display: &mut impl DrawTarget, +) -> Result<(), std::convert::Infallible> { + let mut point = Point::new(start.x, start.y * 256); + + // Pixel(point, c).draw(display)?; + // return Ok(()); + + // https://computergraphics.stackexchange.com/a/10675 + let step = MajorMinor::new(step.major, step.minor); + let delta = MajorMinor::new(delta.major, delta.minor); + + let dx = delta.major.abs(); + let dy = delta.minor.abs(); + + let threshold = dx - 2 * dy; + let e_minor = -2 * dx; + let e_major = 2 * dy; + let mut length = dx + 1; + let mut error = start_error; + + if skip_first { + // Some of the length was consumed by this initial skip iteration. If this is omitted, the + // line will be drawn 1px too long. + last_offset -= 1; + + if error > threshold { + point += step.minor; + error += e_minor; + } + + error += e_major; + point += step.major; + } + + for _i in 0..(length + last_offset) { + // https://computergraphics.stackexchange.com/a/10675 + let draw_p = Point::new(point.x, point.y >> 8); + + Pixel(draw_p, c).draw(display)?; + + if error > threshold { + point += step.minor; + error += e_minor; + } + + error += e_major; + point += step.major; + } + + Ok(()) +} + +/// Minimum distane between a line and a point. +/// +/// From +fn line_point_distance(line: Line, point: Point) -> f32 { + let length = { + let Point { x, y } = line.delta(); + + f32::sqrt((x.pow(2) + y.pow(2)) as f32) + }; + + let x1 = line.start.x; + let x2 = line.end.x; + let x3 = point.x; + + let y1 = line.start.y; + let y2 = line.end.y; + let y3 = point.y; + + let u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) as f32 / length.powi(2); + + let tx = x1 as f32 + u * (x2 - x1) as f32; + let ty = y1 as f32 + u * (y2 - y1) as f32; + + // Tangent intersection point + let tangent = Point::new(tx as i32, ty as i32); + + let Point { x, y } = Line::new(point, tangent).delta(); + + f32::sqrt((x.pow(2) + y.pow(2)) as f32) +} + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, + toggle: bool, + toggle2: bool, + last_offset: i32, +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(200, 200); + // const DISPLAY_SIZE: Size = Size::new(64, 64); + + fn new() -> Self { + let end = Point::new( + Self::DISPLAY_SIZE.width as i32 / 2, + Self::DISPLAY_SIZE.height as i32 / 2, + ); + Self { + start: end + Point::new(10, 15), + end, + // end: start + Point::new(100, 0), + stroke_width: 10, + toggle: true, + toggle2: true, + last_offset: 0, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + Parameter::new("toggle", &mut self.toggle), + Parameter::new("toggle2", &mut self.toggle2), + Parameter::new("last offset", &mut self.last_offset), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let Point { x: _x0, y: _y0 } = self.start; + + // let width = 2 * self.stroke_width as i32 * f32::sqrt((dx * dx + dy * dy) as f32) as i32; + // let width = (self.stroke_width as i32).pow(2) * (dx * dx + dy * dy); + let width = self.stroke_width as i32; + + let _mock_display: MockDisplay = MockDisplay::new(); + + thickline( + display, + Line::new(self.start, self.end), + width, + self.toggle, + self.toggle2, + self.last_offset, + )?; + + // let l = Line::new(self.start, self.end); + + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, width as u32)) + // .draw(display)?; + + // l.perpendicular() + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(5).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/debug-tools/examples/line-parallel.rs b/debug-tools/examples/line-parallel.rs new file mode 100644 index 0000000..c6af450 --- /dev/null +++ b/debug-tools/examples/line-parallel.rs @@ -0,0 +1,268 @@ +use embedded_graphics::{ + geometry::PointExt, + mock_display::MockDisplay, + pixelcolor::Rgb888, + prelude::*, + primitives::{ + common::{LineSide, LinearEquation}, + line::StrokeOffset, + Line, PrimitiveStyle, + }, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; +use integer_sqrt::IntegerSquareRoot; + +#[derive(Debug, Copy, Clone, PartialEq)] +enum LineOffset { + Left, + Center, + Right, +} + +impl LineOffset { + fn widths(self, width: i32) -> (i32, i32) { + match width { + width => { + match self { + Self::Left => (width.saturating_sub(1), 0), + Self::Center => { + let width = width.saturating_sub(1); + + // Right-side bias for even width lines. Move mod2 to first item in the + // tuple to bias to the left instead. + (width / 2, width / 2 + (width % 2)) + } + Self::Right => (width.saturating_sub(1), 0), + } + } + } + } +} + +#[derive(Debug, Clone, Copy)] +struct MajorMinor { + major: T, + minor: T, +} + +impl MajorMinor { + fn new(major: T, minor: T) -> Self { + Self { major, minor } + } +} + +// // From , linked from +// fn dist(line: Line, point: Point) -> f32 { +// let Line { start, .. } = line; + +// let Point { +// x: point_x, +// y: point_y, +// } = point; + +// let point_x = point_x as f32; +// let point_y = point_y as f32; + +// let delta = line.delta(); + +// let slope = delta.y as f32 / delta.x as f32; +// let intercept = start.y as f32 - (slope * start.x as f32); + +// f32::abs(slope * point_x - point_y + intercept) / f32::sqrt(slope.powi(2) + 1.0) +// } + +fn dist(line: Line, point: Point) -> f32 { + let Line { + start: Point { x: x1, y: y1 }, + end: Point { x: x2, y: y2 }, + } = line; + let Point { x: x3, y: y3 } = point; + + let delta = line.end - line.start; + + let denom = (delta.x.pow(2) + delta.y.pow(2)) as f32; + + let u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) as f32 / denom; + + let x = x1 as f32 + u * (x2 - x1) as f32; + let y = y1 as f32 + u * (y2 - y1) as f32; + + let dist = f32::sqrt((x - x3 as f32).powi(2) + (y - y3 as f32).powi(2)); + + dist +} + +fn thickline( + display: &mut impl DrawTarget, + line: Line, + width: i32, +) -> Result<(), std::convert::Infallible> { + let Line { start, end } = line; + + let extents = line.extents(width as u32, StrokeOffset::None); + + let (delta, step, pstep) = { + let delta = end - start; + + let direction = Point::new( + if delta.x >= 0 { 1 } else { -1 }, + if delta.y >= 0 { 1 } else { -1 }, + ); + + let perp_direction = { + // let perp_delta = Point::new(delta.y, -delta.x); + let perp_delta = line.perpendicular(); + let perp_delta = perp_delta.end - perp_delta.start; + + Point::new( + if perp_delta.x >= 0 { 1 } else { -1 }, + if perp_delta.y >= 0 { 1 } else { -1 }, + ) + }; + + // Determine major and minor directions. + if delta.y.abs() >= delta.x.abs() { + ( + MajorMinor::new(delta.y, delta.x), + MajorMinor::new(direction.y_axis(), direction.x_axis()), + MajorMinor::new(perp_direction.y_axis(), perp_direction.x_axis()), + ) + } else { + ( + MajorMinor::new(delta.x, delta.y), + MajorMinor::new(direction.x_axis(), direction.y_axis()), + MajorMinor::new(perp_direction.x_axis(), perp_direction.y_axis()), + ) + } + }; + + // Direction to travel to hit pixel next to current line + let perp_direction = { + // let perp_delta = Point::new(delta.y, -delta.x); + let perp_delta = line.perpendicular(); + let perp_delta = perp_delta.end - perp_delta.start; + + if perp_delta.y.abs() > perp_delta.x.abs() { + Point::new(0, if perp_delta.y >= 0 { 1 } else { -1 }) + } else { + Point::new(if perp_delta.x >= 0 { 1 } else { -1 }, 0) + } + }; + + let mut point = start; + + let dx = delta.major.abs(); + let dy = delta.minor.abs(); + + let threshold = dx - 2 * dy; + let e_minor = -2 * dx; + let e_major = 2 * dy; + let length = dx + 1; + let aa_base_color = Rgb888::MAGENTA; + let mut error = 0; + + let slope = dy as f32 / dx as f32; + let mut bright = 0.5; + + let swap = false; + + println!("==="); + + dbg!(line.delta()); + + for _i in 0..length { + let mul = (bright * 255.0) as u32; + + let aa_color = Rgb888::new( + ((mul * aa_base_color.r() as u32) / 255) as u8, + ((mul * aa_base_color.g() as u32) / 255) as u8, + ((mul * aa_base_color.b() as u32) / 255) as u8, + ); + + Pixel(point, Rgb888::MAGENTA).draw(display)?; + + // if !swap { + // Pixel(point + perp_direction, aa_color).draw(display)?; + // } + + bright = (bright - slope).max(0.0); + + if error > threshold { + point += step.minor; + error += e_minor; + bright = 1.0; + } + + error += e_major; + point += step.major; + } + + Ok(()) +} + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(200, 200); + // const DISPLAY_SIZE: Size = Size::new(64, 64); + + fn new() -> Self { + let end = Point::new( + Self::DISPLAY_SIZE.width as i32 / 2, + Self::DISPLAY_SIZE.height as i32 / 2, + ); + Self { + start: end + Point::new(10, 15), + end, + // end: start + Point::new(100, 0), + stroke_width: 10, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let Point { x: _x0, y: _y0 } = self.start; + + // let width = 2 * self.stroke_width as i32 * f32::sqrt((dx * dx + dy * dy) as f32) as i32; + // let width = (self.stroke_width as i32).pow(2) * (dx * dx + dy * dy); + let width = self.stroke_width as i32; + + let _mock_display: MockDisplay = MockDisplay::new(); + + thickline(display, Line::new(self.start, self.end), width)?; + + // let l = Line::new(self.start, self.end); + + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + // l.perpendicular() + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(5).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/debug-tools/examples/line-perp.rs b/debug-tools/examples/line-perp.rs new file mode 100644 index 0000000..b6bf86f --- /dev/null +++ b/debug-tools/examples/line-perp.rs @@ -0,0 +1,414 @@ +use embedded_graphics::{ + geometry::PointExt, + mock_display::MockDisplay, + pixelcolor::Rgb888, + prelude::*, + primitives::{ + common::{LineSide, LinearEquation}, + line::StrokeOffset, + Line, PrimitiveStyle, + }, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; +use integer_sqrt::IntegerSquareRoot; + +#[derive(Debug, Copy, Clone, PartialEq)] +enum LineOffset { + Left, + Center, + Right, +} + +impl LineOffset { + fn widths(self, width: i32) -> (i32, i32) { + match width { + width => { + match self { + Self::Left => (width.saturating_sub(1), 0), + Self::Center => { + let width = width.saturating_sub(1); + + // Right-side bias for even width lines. Move mod2 to first item in the + // tuple to bias to the left instead. + (width / 2, width / 2 + (width % 2)) + } + Self::Right => (width.saturating_sub(1), 0), + } + } + } + } +} + +#[derive(Debug, Clone, Copy)] +struct MajorMinor { + major: T, + minor: T, +} + +impl MajorMinor { + fn new(major: T, minor: T) -> Self { + Self { major, minor } + } +} + +// // From , linked from +// fn dist(line: Line, point: Point) -> f32 { +// let Line { start, .. } = line; + +// let Point { +// x: point_x, +// y: point_y, +// } = point; + +// let point_x = point_x as f32; +// let point_y = point_y as f32; + +// let delta = line.delta(); + +// let slope = delta.y as f32 / delta.x as f32; +// let intercept = start.y as f32 - (slope * start.x as f32); + +// f32::abs(slope * point_x - point_y + intercept) / f32::sqrt(slope.powi(2) + 1.0) +// } + +// From +// Slower, but doesn't give NaNs as above solution does sometimes +fn dist(line: Line, point: Point) -> f32 { + let Line { + start: Point { x: x1, y: y1 }, + end: Point { x: x2, y: y2 }, + } = line; + + let Point { x: px, y: py } = point; + + let x1 = x1 as f32; + let y1 = y1 as f32; + let x2 = x2 as f32; + let y2 = y2 as f32; + let px = px as f32; + let py = py as f32; + + let delta = line.delta(); + + let length_sq = delta.length_squared(); + let length = f32::sqrt(length_sq as f32); + + let dx = delta.x as f32 / length; + let dy = delta.y as f32 / length; + + let p = dx * (px - x1) + dy * (py - y1); + + if p < 0.0 { + let dx = px - x1; + let dy = py - y1; + return length; + } else if p > length { + let dx = px - x2; + let dy = py - y2; + return length; + } + + return f32::abs(dy * (px - x1) - dx * (py - y1)); +} + +fn perpendicular( + display: &mut impl DrawTarget, + line: Line, + (left_extent, right_extent): (Line, Line), + x0: i32, + y0: i32, + delta: MajorMinor, + step: MajorMinor, + einit: i32, + width: i32, + winit: i32, + extra: bool, +) -> Result<(), std::convert::Infallible> { + let mut point = Point::new(x0, y0); + + if width == 0 { + return Ok(()); + } + + let dx = delta.major; + let dy = delta.minor; + + let sign = match (step.major, step.minor) { + (Point { x: -1, y: 0 }, Point { x: 0, y: 1 }) => -1, + (Point { x: 0, y: -1 }, Point { x: -1, y: 0 }) => -1, + (Point { x: 1, y: 0 }, Point { x: 0, y: -1 }) => -1, + (Point { x: 0, y: 1 }, Point { x: 1, y: 0 }) => -1, + _ => 1, + }; + + let dx = dx.abs(); + let dy = dy.abs(); + + let threshold = dx - 2 * dy; + // E_diag + let e_minor = -2 * dx; + // E_square + let e_major = 2 * dy; + + let mut error = einit * sign; + + let (side_check_left, side_check_right) = (LineSide::Left, LineSide::Right); + + let (_width_l, _width_r) = LineOffset::Center.widths(width); + + let (c_left, c_right) = if extra { + (Rgb888::RED, Rgb888::GREEN) + } else { + (Rgb888::CSS_CORNFLOWER_BLUE, Rgb888::YELLOW) + }; + let c_left = Rgb888::WHITE; + let c_right = c_left; + + // Add one to width so we get an extra iteration for the AA edge + let wthr = (width + 1).pow(2) * (dx.pow(2) + dy.pow(2)); + let init_offset = dx + dy - (winit * sign); + let mut tk = init_offset; + // let mut tk: i32 = 0; + + // dbg!(wthr); + + println!("==="); + + // Perpendicular iteration + while tk.pow(2) <= wthr { + Pixel(point, c_left).draw(display)?; + + if error > threshold { + point += step.major; + error += e_minor; + tk += 2 * dy; + } + + error += e_major; + point += step.minor; + tk += 2 * dx; + + if tk.pow(2) > wthr { + let fract = tk.pow(2) as u32 * 255 / (wthr as u32); + + let fract = { + // There's this weird division of 1.5 to make the AA look correct. This magic value + // is the 8 bit scaler 255 / 1.5. I haven't got to the bottom of why it must be 1.5 + // yet. Maybe something to do with Bresenham's errors being at most 0.5 away from + // pixel centers and everything being multiplied by 2? + let two_thirds_255 = 170.0; + + let thickness_ratio = (tk.pow(2) as f32 * two_thirds_255) / (wthr as f32); + let thickness_ratio = thickness_ratio % two_thirds_255; + + (255.0 - thickness_ratio * _width_l as f32) as u32 + }; + + let c = Rgb888::new( + ((fract * c_left.r() as u32) / 255) as u8, + ((fract * c_left.g() as u32) / 255) as u8, + ((fract * c_left.b() as u32) / 255) as u8, + ); + + Pixel(point, c).draw(display)?; + } + } + + let mut point = Point::new(x0, y0); + let mut error = einit * -sign; + + let mut tk = dx + dy + (winit * sign); + + while tk.pow(2) <= wthr { + Pixel(point, c_right).draw(display)?; + + if error > threshold { + point -= step.major; + error += e_minor; + tk += 2 * dy; + } + + error += e_major; + point -= step.minor; + tk += 2 * dx; + } + + Ok(()) +} + +fn thickline( + display: &mut impl DrawTarget, + line: Line, + width: i32, +) -> Result<(), std::convert::Infallible> { + let Line { start, end } = line; + + let extents = line.extents(width as u32, StrokeOffset::None); + + let (delta, step, pstep) = { + let delta = end - start; + + let direction = Point::new( + if delta.x >= 0 { 1 } else { -1 }, + if delta.y >= 0 { 1 } else { -1 }, + ); + + let perp_direction = { + // let perp_delta = Point::new(delta.y, -delta.x); + let perp_delta = line.perpendicular(); + let perp_delta = perp_delta.end - perp_delta.start; + + Point::new( + if perp_delta.x >= 0 { 1 } else { -1 }, + if perp_delta.y >= 0 { 1 } else { -1 }, + ) + }; + + // Determine major and minor directions. + if delta.y.abs() >= delta.x.abs() { + ( + MajorMinor::new(delta.y, delta.x), + MajorMinor::new(direction.y_axis(), direction.x_axis()), + MajorMinor::new(perp_direction.y_axis(), perp_direction.x_axis()), + ) + } else { + ( + MajorMinor::new(delta.x, delta.y), + MajorMinor::new(direction.x_axis(), direction.y_axis()), + MajorMinor::new(perp_direction.x_axis(), perp_direction.y_axis()), + ) + } + }; + + let mut p_error = 0; + let mut error = 0; + let mut point = start; + + let dx = delta.major.abs(); + let dy = delta.minor.abs(); + + let threshold = dx - 2 * dy; + let e_minor = -2 * dx; + let e_major = 2 * dy; + let length = dx + 1; + + let _greys = 255.0 / dx as f32; + + let skele_color = Rgb888::MAGENTA; + + for _i in 0..length { + // let draw_skele = i % 2 == 0; + let draw_skele = false; + + perpendicular( + display, line, extents, point.x, point.y, delta, pstep, p_error, width, error, false, + )?; + + if draw_skele { + Pixel(point, skele_color).draw(display)?; + } + + if error > threshold { + point += step.minor; + error += e_minor; + + if p_error >= threshold { + if width > 1 { + perpendicular( + display, + line, + extents, + point.x, + point.y, + delta, + pstep, + p_error + e_minor + e_major, + width, + error, + true, + )?; + + if draw_skele { + Pixel(point, skele_color).draw(display)?; + } + } + + p_error += e_minor; + } + + p_error += e_major; + } + + error += e_major; + point += step.major; + } + + Ok(()) +} + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(200, 200); + // const DISPLAY_SIZE: Size = Size::new(64, 64); + + fn new() -> Self { + let end = Point::new( + Self::DISPLAY_SIZE.width as i32 / 2, + Self::DISPLAY_SIZE.height as i32 / 2, + ); + Self { + start: end + Point::new(10, 15), + end, + // end: start + Point::new(100, 0), + stroke_width: 10, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let Point { x: _x0, y: _y0 } = self.start; + + // let width = 2 * self.stroke_width as i32 * f32::sqrt((dx * dx + dy * dy) as f32) as i32; + // let width = (self.stroke_width as i32).pow(2) * (dx * dx + dy * dy); + let width = self.stroke_width as i32; + + let _mock_display: MockDisplay = MockDisplay::new(); + + thickline(display, Line::new(self.start, self.end), width)?; + + // let l = Line::new(self.start, self.end); + + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + // l.perpendicular() + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(5).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/debug-tools/examples/line.rs b/debug-tools/examples/line.rs index 54a14cb..2012bbc 100644 --- a/debug-tools/examples/line.rs +++ b/debug-tools/examples/line.rs @@ -1,5 +1,5 @@ use embedded_graphics::{ - pixelcolor::Rgb565, + pixelcolor::{Gray8, Rgb565}, prelude::*, primitives::{Line, PrimitiveStyle}, }; @@ -12,6 +12,235 @@ struct LineDebug { stroke_width: u32, } +fn thin_octant1( + display: &mut SimulatorDisplay, + x0: i32, + y0: i32, + dx: i32, + dy: i32, +) -> Result<(), std::convert::Infallible> { + let mut error: i32 = 0; + let mut y = y0; + let mut x = x0; + let threshold = dx - 2 * dy; + let e_major = 2 * dx; + let e_minor = 2 * dy; + let length = dx; + + for _ in 1..length { + // Pixel(Point::new(x, y - 3), Rgb565::new(ass as u8, 0, 0)).draw(display)?; + Pixel(Point::new(x, y), Rgb565::GREEN).draw(display)?; + + if error > threshold { + y += 1; + error -= e_major; + } + + error += e_minor; + x += 1; + } + + Ok(()) +} + +fn plot( + display: &mut SimulatorDisplay, + x: f32, + y: f32, + c: f32, +) -> Result<(), std::convert::Infallible> { + let c = Rgb565::new(0, (c * 255.0) as u8, 0); + + Pixel(Point::new(x as i32, y as i32), c).draw(display) +} + +// integer part of x +fn ipart(x: f32) -> f32 { + f32::floor(x) +} + +fn round(x: f32) -> f32 { + f32::trunc(x + 0.5) +} + +// fractional part of x +fn fpart(x: f32) -> f32 { + x - f32::floor(x) +} + +fn rfpart(x: f32) -> f32 { + 1.0 - f32::fract(x) +} + +fn wu( + display: &mut SimulatorDisplay, + x0: i32, + y0: i32, + x1: i32, + y1: i32, +) -> Result<(), std::convert::Infallible> { + let mut x0 = x0 as f32; + let mut y0 = y0 as f32; + let mut x1 = x1 as f32; + let mut y1 = y1 as f32; + + let mut steep = f32::abs(y1 - y0) > f32::abs(x1 - x0); + + if steep { + core::mem::swap(&mut x0, &mut y0); + core::mem::swap(&mut x1, &mut y1); + } + if x0 > x1 { + core::mem::swap(&mut x0, &mut x1); + core::mem::swap(&mut y0, &mut y1); + } + + let mut dx = x1 - x0; + let mut dy = y1 - y0; + let mut gradient = dy / dx; + if dx == 0.0 { + let mut gradient = 1.0; + } + + // handle first endpoint + let mut xend = round(x0); + let mut yend = y0 + gradient * (xend - x0); + let mut xgap = rfpart(x0 + 0.5); + let mut xpxl1 = xend; // this will be used in the main loop + let mut ypxl1 = ipart(yend); + if steep { + plot(display, ypxl1, xpxl1, rfpart(yend) * xgap)?; + plot(display, ypxl1 + 1.0, xpxl1, fpart(yend) * xgap)?; + } else { + plot(display, xpxl1, ypxl1, rfpart(yend) * xgap)?; + plot(display, xpxl1, ypxl1 + 1.0, fpart(yend) * xgap)?; + } + let mut intery = yend + gradient; // first y-intersection for the main loop + + // handle second endpoint + let mut xend = round(x1); + let mut yend = y1 + gradient * (xend - x1); + let mut xgap = fpart(x1 + 0.5); + let mut xpxl2 = xend; //this will be used in the main loop + let mut ypxl2 = ipart(yend); + if steep { + plot(display, ypxl2, xpxl2, rfpart(yend) * xgap)?; + plot(display, ypxl2 + 1.0, xpxl2, fpart(yend) * xgap)?; + } else { + plot(display, xpxl2, ypxl2, rfpart(yend) * xgap)?; + plot(display, xpxl2, ypxl2 + 1.0, fpart(yend) * xgap)?; + } + + // main loop + if steep { + for x in (xpxl1 as i32 + 1)..(xpxl2 as i32 - 1) { + plot(display, ipart(intery), x as f32, rfpart(intery))?; + plot(display, ipart(intery) + 1.0, x as f32, fpart(intery))?; + intery = intery + gradient; + } + } else { + for x in (xpxl1 as i32 + 1)..(xpxl2 as i32 - 1) { + plot(display, x as f32, ipart(intery), rfpart(intery))?; + plot(display, x as f32, ipart(intery) + 1.0, fpart(intery))?; + intery = intery + gradient; + } + } + + Ok(()) +} + +// //returns 1 - fractional part of number +// fn rfPartOfNumber(x: f32) -> f32 { +// return 1.0 - f32::fract(x); +// } + +// fn drawPixel( +// display: &mut SimulatorDisplay, +// x: f32, +// y: f32, +// color: f32, +// ) -> Result<(), std::convert::Infallible> { +// Pixel( +// Point::new(x as i32, y as i32), +// Rgb565::new(0, (color * 255.0) as u8, 0), +// ) +// .draw(display) +// } + +// fn drawAALine( +// display: &mut SimulatorDisplay, +// mut x0: i32, +// mut y0: i32, +// mut x1: i32, +// mut y1: i32, +// ) -> Result<(), std::convert::Infallible> { +// let steep = (y1 - y0).abs() > (x1 - x0).abs(); + +// // swap the co-ordinates if slope > 1 or we +// // draw backwards +// if steep { +// core::mem::swap(&mut x0, &mut y0); +// core::mem::swap(&mut x1, &mut y1); +// } +// if x0 > x1 { +// core::mem::swap(&mut x0, &mut x1); +// core::mem::swap(&mut y0, &mut y1); +// } + +// //compute the slope +// let dx = x1 - x0; +// let dy = y1 - y0; +// let mut gradient: f32 = dy as f32 / dx as f32; +// if dx == 0 { +// gradient = 1.0; +// } + +// let xpxl1 = x0; +// let xpxl2 = x1; +// let mut intersectY = y0 as f32; + +// // main loop +// if steep { +// for x in xpxl1..=xpxl2 { +// // pixel coverage is determined by fractional +// // part of y co-ordinate +// drawPixel( +// display, +// f32::trunc(intersectY), +// x as f32, +// rfPartOfNumber(intersectY), +// )?; +// drawPixel( +// display, +// f32::trunc(intersectY) - 1.0, +// x as f32, +// f32::fract(intersectY), +// )?; +// intersectY += gradient; +// } +// } else { +// for x in xpxl1..=xpxl2 { +// // pixel coverage is determined by fractional +// // part of y co-ordinate +// drawPixel( +// display, +// x as f32, +// f32::trunc(intersectY), +// rfPartOfNumber(intersectY), +// )?; +// drawPixel( +// display, +// x as f32, +// f32::trunc(intersectY) - 1.0, +// f32::fract(intersectY), +// )?; +// intersectY += gradient; +// } +// } + +// Ok(()) +// } + impl App for LineDebug { type Color = Rgb565; const DISPLAY_SIZE: Size = Size::new(256, 256); @@ -36,12 +265,28 @@ impl App for LineDebug { &self, display: &mut SimulatorDisplay, ) -> Result<(), std::convert::Infallible> { - Line::new(self.start, self.end) - .into_styled(PrimitiveStyle::with_stroke( - Rgb565::GREEN, - self.stroke_width, - )) - .draw(display) + let line = Line::new(self.start, self.end); + + let delta = line.delta(); + + // thin_octant1(display, line.start.x, line.start.y, delta.x, delta.y)?; + wu(display, line.start.x, line.start.y, delta.x, delta.y)?; + + // { + // let Line { + // start: Point { x: x0, y: y0 }, + // end: Point { x: x1, y: y1 }, + // } = line; + + // drawAALine(display, x0, y0, x1, y1)?; + // } + + // Line::new(self.start, self.end) + // .points() + // .map(|point| Pixel(point, Rgb565::GREEN)) + // .draw(display) + + Ok(()) } } diff --git a/debug-tools/examples/mul-both-axes.rs b/debug-tools/examples/mul-both-axes.rs new file mode 100644 index 0000000..b006ba2 --- /dev/null +++ b/debug-tools/examples/mul-both-axes.rs @@ -0,0 +1,428 @@ +use embedded_graphics::{ + geometry::PointExt, mock_display::MockDisplay, pixelcolor::Rgb888, prelude::*, primitives::Line, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; + +#[derive(Debug, Clone, Copy)] +struct MajorMinor { + major: T, + minor: T, +} + +impl MajorMinor { + fn new(major: T, minor: T) -> Self { + Self { major, minor } + } +} + +fn thickline( + display: &mut impl DrawTarget, + line: Line, + width: i32, + extra: bool, + phase: i32, +) -> Result<(), std::convert::Infallible> { + if width == 0 { + return Ok(()); + } + + // // Draw line using existing algorithm to check against + // if extra { + // let mut line = line; + + // // line.start.y += width * 2; + // // line.end.y += width * 2; + + // line.into_styled(embedded_graphics::primitives::PrimitiveStyle::with_stroke( + // Rgb888::WHITE, + // 1, + // )) + // .draw(display)?; + // } + + let original_parallel = line; + let original_parallel_delta = original_parallel.delta(); + let original_seed = original_parallel.perpendicular(); + let original_seed_delta = original_seed.delta(); + let original_delta_majorminor = + if original_parallel_delta.y.abs() >= original_parallel_delta.x.abs() { + MajorMinor::new(original_parallel_delta.y, original_parallel_delta.x) + } else { + MajorMinor::new(original_parallel_delta.x, original_parallel_delta.y) + }; + let original_seed_step = Point::new( + if original_seed_delta.x >= 0 { 1 } else { -1 }, + if original_seed_delta.y >= 0 { 1 } else { -1 }, + ); + let original_seed_step_majorminor = + if original_seed_delta.y.abs() >= original_seed_delta.x.abs() { + MajorMinor::new(original_seed_step.y_axis(), original_seed_step.x_axis()) + } else { + MajorMinor::new(original_seed_step.x_axis(), original_seed_step.y_axis()) + }; + + let line = Line::new(line.start * 256, line.end * 256); + + let seed_line = line.perpendicular(); + let seed_delta = seed_line.delta(); + + let parallel_delta = line.delta(); + + let parallel_is_y_major = line.delta().y.abs() >= line.delta().x.abs(); + let seed_is_y_major = seed_delta.y.abs() >= seed_delta.x.abs(); + + let seed_step = Point::new( + if seed_delta.x >= 0 { 1 } else { -1 }, + if seed_delta.y >= 0 { 1 } else { -1 }, + ) * 256; + + let parallel_step = Point::new( + if parallel_delta.x >= 0 { 1 } else { -1 }, + if parallel_delta.y >= 0 { 1 } else { -1 }, + ) * 256; + + // --- + + let seed_delta_majorminor = if seed_is_y_major { + MajorMinor::new(seed_delta.y / 256, seed_delta.x) + } else { + MajorMinor::new(seed_delta.x / 256, seed_delta.y) + }; + let parallel_delta_majorminor = if parallel_is_y_major { + MajorMinor::new(parallel_delta.y / 256, parallel_delta.x) + } else { + MajorMinor::new(parallel_delta.x / 256, parallel_delta.y) + }; + + // Plain old boring multiplied by 256 + let seed_step_majorminor = if seed_is_y_major { + MajorMinor::new(seed_step.y_axis(), seed_step.x_axis()) + } else { + MajorMinor::new(seed_step.x_axis(), seed_step.y_axis()) + }; + let parallel_step_256_majorminor = if parallel_is_y_major { + MajorMinor::new(parallel_step.y_axis(), parallel_step.x_axis()) + } else { + MajorMinor::new(parallel_step.x_axis(), parallel_step.y_axis()) + }; + + // Using line slope + let seed_step_majorminor = if seed_is_y_major { + MajorMinor::new( + seed_step.y_axis(), + Point::new( + (original_seed_delta.x * 256) / (original_seed_delta.y * seed_step.y.signum()), + 0, + ), + ) + } else { + MajorMinor::new( + seed_step.x_axis(), + Point::new( + 0, + (original_seed_delta.y * 256) / (original_seed_delta.x * seed_step.x.signum()), + ), + ) + }; + let parallel_step_majorminor = if parallel_is_y_major { + MajorMinor::new( + parallel_step.y_axis(), + Point::new( + (original_parallel_delta.x * 256) + / (original_parallel_delta.y * parallel_step.y.signum()), + 0, + ), + ) + } else { + MajorMinor::new( + parallel_step.x_axis(), + Point::new( + 0, + (original_parallel_delta.y * 256) + / (original_parallel_delta.x * parallel_step.x.signum()), + ), + ) + }; + + // --- + + let dx = seed_delta_majorminor.major.abs(); + let dy = seed_delta_majorminor.minor.abs(); + let parallel_dx = parallel_delta_majorminor.major.abs(); + let parallel_dy = parallel_delta_majorminor.minor.abs(); + + // http://kt8216.unixcab.org/murphy/index.html calls e_minor E_diag, and e_major E_square + let e_minor = -2 * dx; + let e_major = 2 * dy; + let parallel_e_minor = -2 * parallel_dx; + let parallel_e_major = 2 * parallel_dy; + + let mut seed_line_error = 2 * dy - dx; + let mut parallel_start_error = 2 * parallel_dy - parallel_dx; + let mut point = seed_line.start; + let mut prev = point; + let mut parallel_point = line.start; + + for i in 0..width { + let p = point / 256; + + // Pixel(p, Rgb888::RED).draw(display)?; + + let aa_p = point / 256 - original_seed_step_majorminor.minor; + + let aa_c = { + let c = Rgb888::CSS_GOLDENROD; + let background = Rgb888::BLACK; + + let mul = (if seed_is_y_major { + point.x & 255 + } else { + point.y & 255 + }) as u8; + + // // Some octants need the AA direction to go the other way + // let mul = if swap_aa_direction { 255 - mul } else { mul }; + + Rgb888::new( + integer_lerp(c.r(), background.r(), mul), + integer_lerp(c.g(), background.g(), mul), + integer_lerp(c.b(), background.b(), mul), + ) + }; + + Pixel(aa_p, aa_c).draw(display)?; + + // Draw parallel line + { + let mut parallel_line_error = parallel_start_error; + let mut point = parallel_point; + + for i in 0..original_delta_majorminor.major.abs() { + let p = point / 256; + + Pixel(p, Rgb888::CSS_AQUAMARINE).draw(display)?; + + if parallel_line_error > 0 { + point += parallel_step_majorminor.minor; + parallel_line_error += parallel_e_minor; + } + + point += parallel_step_majorminor.major; + parallel_line_error += parallel_e_major; + } + } + + if seed_line_error > 0 { + point += seed_step_majorminor.minor; + parallel_point += seed_step_majorminor.minor; + seed_line_error += e_minor; + } + + point += seed_step_majorminor.major; + parallel_point += seed_step_majorminor.major; + seed_line_error += e_major; + + prev = p; + } + + // Draw AA line + { + let mut parallel_line_error = parallel_start_error; + let mut point = parallel_point; + + for i in 0..original_delta_majorminor.major.abs() { + let aa_p = point / 256; + + let aa_c = { + let c = Rgb888::CSS_GOLDENROD; + let background = Rgb888::BLACK; + + let mul = (if parallel_is_y_major { + point.x & 255 + } else { + point.y & 255 + }) as u8; + + // // Some octants need the AA direction to go the other way + // let mul = if swap_aa_direction { 255 - mul } else { mul }; + + Rgb888::new( + integer_lerp(c.r(), background.r(), mul), + integer_lerp(c.g(), background.g(), mul), + integer_lerp(c.b(), background.b(), mul), + ) + }; + + Pixel(aa_p, aa_c).draw(display)?; + + if parallel_line_error > 0 { + point += parallel_step_majorminor.minor; + parallel_line_error += parallel_e_minor; + } + + point += parallel_step_majorminor.major; + parallel_line_error += parallel_e_major; + } + } + + Ok(()) +} + +/// Integer-only LERP with 8 bits of precision. +/// +/// Thanks to for the inspiration. +fn integer_lerp(a: u8, b: u8, f: u8) -> u8 { + let a = u16::from(a); + let b = u16::from(b); + let f = u16::from(f); + + let res = (a * (u16::from(u8::MAX) - f) + b * f) >> 8; + + res as u8 +} + +fn parallel_line_2( + start: Point, + line_is_y_major: bool, + step: MajorMinor, + delta: MajorMinor, + c: Rgb888, + display: &mut impl DrawTarget, + extra: bool, + initial_error: i32, +) -> Result<(), std::convert::Infallible> { + let mut point = start; + + point += step.major * 2; + + let dx = delta.major; + let dy = delta.minor; + + let e_minor = -2 * dx; + let e_major = 2 * dy; + let length = dx; + // Setting this to zero causes the first segment before the minor step to be too long + // let mut error = 2 * dy - dx; + let mut error = initial_error; + + for _i in 0..length { + // let p = Point::new( + // if line_is_y_major { + // point.x >> 8 + // } else { + // point.x + // }, + // if line_is_y_major { + // point.y + // } else { + // point.y >> 8 + // }, + // ); + + let p = point; + + Pixel(p, c).draw(display)?; + + // Draws a pixel connecting a diagonal move into a solid stairstep-looking piece. This is + // required for the additional diagonal move lines that are drawn when stepping in both the + // major and minor directions in the seed line. + if extra { + let p = point + step.minor; + + let p = Point::new( + if line_is_y_major { p.x >> 8 } else { p.x }, + if line_is_y_major { p.y } else { p.y >> 8 }, + ); + + Pixel(p, c).draw(display)?; + } + + if error > 0 { + point += step.minor; + error += e_minor; + } + + point += step.major; + error += e_major; + } + + Ok(()) +} + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, + phase: i32, + extra: bool, +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(200, 200); + // const DISPLAY_SIZE: Size = Size::new(64, 64); + + fn new() -> Self { + let end = Point::new( + Self::DISPLAY_SIZE.width as i32 / 2, + Self::DISPLAY_SIZE.height as i32 / 2, + ); + Self { + start: end - Point::new(80, 35), + end, + stroke_width: 10, + phase: 0, + extra: true, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + Parameter::new("phase", &mut self.phase), + Parameter::new("extra", &mut self.extra), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let Point { x: _x0, y: _y0 } = self.start; + + // let width = 2 * self.stroke_width as i32 * f32::sqrt((dx * dx + dy * dy) as f32) as i32; + // let width = (self.stroke_width as i32).pow(2) * (dx * dx + dy * dy); + let width = self.stroke_width as i32; + + let _mock_display: MockDisplay = MockDisplay::new(); + + thickline( + display, + Line::new(self.start, self.end), + width, + self.extra, + self.phase, + )?; + + // let l = Line::new(self.start, self.end); + + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, width as u32)) + // .draw(display)?; + + // l.perpendicular() + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(5).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/debug-tools/examples/thick-line-mul-256.rs b/debug-tools/examples/thick-line-mul-256.rs new file mode 100644 index 0000000..59291d0 --- /dev/null +++ b/debug-tools/examples/thick-line-mul-256.rs @@ -0,0 +1,711 @@ +use embedded_graphics::{ + geometry::PointExt, mock_display::MockDisplay, pixelcolor::Rgb888, prelude::*, primitives::Line, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window}; +use framework::prelude::*; + +#[derive(Debug, Clone, Copy)] +struct MajorMinor { + major: T, + minor: T, +} + +impl MajorMinor { + fn new(major: T, minor: T) -> Self { + Self { major, minor } + } +} + +fn thickline( + display: &mut impl DrawTarget, + line: Line, + width: i32, + extra: bool, + phase: i32, +) -> Result<(), std::convert::Infallible> { + if width == 0 { + return Ok(()); + } + + // Draw line using existing algorithm to check against + // if extra { + // let mut line = line; + + // // line.start.y += width * 2; + // // line.end.y += width * 2; + + // line.into_styled(embedded_graphics::primitives::PrimitiveStyle::with_stroke( + // Rgb888::WHITE, + // width as u32, + // )) + // .draw(display)?; + // } + + let non_mul_line = line; + let non_mul_perpendicular_delta = line.perpendicular().delta(); + let seed_line = line.perpendicular(); + + let parallel_is_y_major = line.delta().y.abs() >= line.delta().x.abs(); + + let seed_is_y_major = + non_mul_perpendicular_delta.y.abs() >= non_mul_perpendicular_delta.x.abs(); + + // Using a block to isolate mutability + let mul_line = { + let mut line = line; + + // Multiply minor direction by 256 so we get AA resolution in lower 8 bits + if parallel_is_y_major { + line.start.x *= 256; + line.end.x *= 256; + } else { + line.start.y *= 256; + line.end.y *= 256; + } + + line + }; + + let parallel_delta = line.delta(); + + let parallel_delta_mul = mul_line.delta(); + + let mul_seed = { + let mut line = seed_line; + + if seed_is_y_major { + line.start.x *= 256; + line.end.x *= 256; + } else { + line.start.y *= 256; + line.end.y *= 256; + } + + line + }; + + let mul_seed_delta = mul_seed.delta(); + + let seed_step = Point::new( + if mul_seed_delta.x >= 0 { 1 } else { -1 }, + if mul_seed_delta.y >= 0 { 1 } else { -1 }, + ); + + let (thickness_majorminor, seed_line_delta, seed_step, seed_step_full) = if seed_is_y_major { + ( + MajorMinor::new(non_mul_perpendicular_delta.y, non_mul_perpendicular_delta.x), + MajorMinor::new(mul_seed_delta.y, mul_seed_delta.x), + MajorMinor::new( + seed_step.y_axis(), + Point::new((mul_seed_delta.x / mul_seed_delta.y).abs(), 0).component_mul(seed_step), + ), + MajorMinor::new(seed_step.y_axis(), seed_step.x_axis() * 256), + ) + } + // X-major line (i.e. X delta is longer than Y) + else { + ( + MajorMinor::new(non_mul_perpendicular_delta.x, non_mul_perpendicular_delta.y), + MajorMinor::new(mul_seed_delta.x, mul_seed_delta.y), + MajorMinor::new( + seed_step.x_axis(), + Point::new(0, (mul_seed_delta.y / mul_seed_delta.x).abs()).component_mul(seed_step), + ), + MajorMinor::new(seed_step.x_axis(), seed_step.y_axis() * 256), + ) + }; + + // --- + + let parallel_step = Point::new( + if parallel_delta.x >= 0 { 1 } else { -1 }, + if parallel_delta.y >= 0 { 1 } else { -1 }, + ); + + let (parallel_delta, parallel_step, parallel_step_mul, parallel_step_full) = + if parallel_is_y_major { + ( + MajorMinor::new(parallel_delta.y.abs(), parallel_delta.x.abs()), + MajorMinor::new( + parallel_step.y_axis(), + parallel_step.x_axis(), + // Point::new((parallel_delta_mul.x / parallel_delta_mul.y).abs(), 0).component_mul(parallel_step), + ), + MajorMinor::new( + parallel_step.y_axis(), + Point::new((parallel_delta_mul.x / parallel_delta_mul.y).abs(), 0) + .component_mul(parallel_step), + ), + MajorMinor::new(parallel_step.y_axis(), parallel_step.x_axis()), + ) + } else { + ( + MajorMinor::new(parallel_delta.x.abs(), parallel_delta.y.abs()), + MajorMinor::new( + parallel_step.x_axis(), + parallel_step.y_axis(), + // Point::new(0, (parallel_delta_mul.y / parallel_delta_mul.x).abs()).component_mul(parallel_step), + ), + MajorMinor::new( + parallel_step.x_axis(), + Point::new(0, (parallel_delta_mul.y / parallel_delta_mul.x).abs()) + .component_mul(parallel_step), + ), + MajorMinor::new(parallel_step.x_axis(), parallel_step.y_axis()), + ) + }; + + dbg!( + parallel_delta, + parallel_step, + parallel_step_mul, + parallel_step_full + ); + + todo!(); + + // --- + + let dx = seed_line_delta.major.abs(); + let dy = seed_line_delta.minor.abs(); + + // Using non-multiplied line delta otherwise thickness threshold runs into overflow issues (I + // think? It ended up negative in testing) + let thickness_dx = thickness_majorminor.major.abs(); + let thickness_dy = thickness_majorminor.minor.abs(); + + // let threshold = dx - 2 * dy; + // http://kt8216.unixcab.org/murphy/index.html calls e_minor E_diag, and e_major E_square + let e_minor = -2 * dx; + let e_major = 2 * dy; + let mut seed_line_error = 2 * dy - dx; + + // Subtract 1 if using AA so 1px wide lines are _only_ drawn with AA - no solid fill + let thickness_threshold = + ((width - 1) * 2).pow(2) * non_mul_perpendicular_delta.length_squared(); + // Add the first line drawn to the thickness. If this is left at zero, an extra line will be + // drawn as the lines are drawn before checking for thickness. + let mut thickness_accumulator = 2 * thickness_dx; + + // // This fixes the phasing for parallel lines on the left side of the base line for the octants + // // where the line perpendicular moves "away" from the line body. + // let flip = if seed_line_step.minor == -parallel_step.major { + // -1 + // } else { + // 1 + // }; + + let swap_aa_direction = parallel_step_full.minor.x < 0 || parallel_step_full.minor.y < 0; + + let mut mul_point = non_mul_line.start; + let mut seed_point = mul_seed.start; + + let mut prev = Point::new( + if seed_is_y_major { + seed_point.x >> 8 + } else { + seed_point.x + }, + if seed_is_y_major { + seed_point.y + } else { + seed_point.y >> 8 + }, + ); + + let mut parallel_error = 2 * parallel_delta.minor + parallel_delta.major; + // let mut parallel_error = 0; + + while thickness_accumulator.pow(2) <= thickness_threshold { + let p = Point::new( + if seed_is_y_major { + seed_point.x >> 8 + } else { + seed_point.x + }, + if seed_is_y_major { + seed_point.y + } else { + seed_point.y >> 8 + }, + ); + + let background = Rgb888::BLACK; + let c = Rgb888::RED; + + Pixel(p, c).draw(display)?; + + let aa_c = { + let c = Rgb888::GREEN; + + let mul = (if seed_is_y_major { + seed_point.x & 255 + } else { + seed_point.y & 255 + }) as u8; + + // Some octants need the AA direction to go the other way + let mul = if swap_aa_direction { 255 - mul } else { mul }; + + Rgb888::new( + integer_lerp(c.r(), background.r(), mul), + integer_lerp(c.g(), background.g(), mul), + integer_lerp(c.b(), background.b(), mul), + ) + }; + + let aa_p = { + let p = seed_point - seed_step_full.minor; + + Point::new( + if seed_is_y_major { p.x >> 8 } else { p.x }, + if seed_is_y_major { p.y } else { p.y >> 8 }, + ) + }; + + Pixel(aa_p, aa_c).draw(display)?; + + // Move seed line in minor direction + if seed_line_error > 0 { + seed_line_error += e_minor; + + seed_point += seed_step.minor; + + if prev.x != p.x { + mul_point += parallel_step_full.major; + + if parallel_error > 0 { + parallel_error += -2 * parallel_delta.major; + } + + parallel_error += 2 * parallel_delta.minor; + } + + parallel_line_2( + p, + parallel_is_y_major, + parallel_step, + parallel_delta, + Rgb888::CSS_AQUAMARINE, + display, + false, + parallel_error, + )?; + + thickness_accumulator += 2 * thickness_dy; + } else { + // parallel_line_2( + // mul_point, + // parallel_is_y_major, + // parallel_step, + // parallel_delta, + // Rgb888::CSS_AQUAMARINE, + // display, + // false, + // )?; + } + + seed_line_error += e_major; + thickness_accumulator += 2 * thickness_dx; + seed_point += seed_step.major * 3; + mul_point += parallel_step_full.minor * -1 * 3; + + prev = p; + } + + parallel_line_2_aa( + mul_point, + parallel_is_y_major, + parallel_step_mul, + parallel_delta, + Rgb888::CSS_GOLDENROD, + display, + false, + phase, + )?; + + // if extra { + // // Final AA line + // parallel_line_aa( + // mul_point, + // parallel_is_y_major, + // parallel_step, + // parallel_delta, + // // Rgb888::CSS_GOLDENROD, + // Rgb888::CSS_AQUAMARINE, + // swap_aa_direction, + // display, + // )?; + + // // First AA line + // parallel_line_aa( + // mul_line.start + parallel_step_full.minor, + // parallel_is_y_major, + // parallel_step, + // parallel_delta, + // // Rgb888::CSS_GOLDENROD, + // Rgb888::CSS_AQUAMARINE, + // !swap_aa_direction, + // display, + // )?; + // } + + // { + // let line = Line::new(mul_line.start, mul_point); + + // let delta = line.delta(); + + // let step = Point::new( + // if delta.x >= 0 { 1 } else { -1 }, + // if delta.y >= 0 { 1 } else { -1 }, + // ); + + // let (delta, step) = if !seed_is_y_major { + // ( + // MajorMinor::new(delta.y.abs(), delta.x.abs()), + // MajorMinor::new( + // step.y_axis(), + // Point::new((delta.x / delta.y).abs(), 0).component_mul(step), + // ), + // ) + // } else { + // ( + // MajorMinor::new(delta.x.abs(), delta.y.abs()), + // MajorMinor::new( + // step.x_axis(), + // Point::new(0, (delta.y / delta.x).abs()).component_mul(step), + // ), + // ) + // }; + + // dbg!(delta, step, line, seed_is_y_major); + + // parallel_line_2( + // line.start, + // !seed_is_y_major, + // step, + // delta, + // Rgb888::RED, + // display, + // false, + // )?; + // } + + Ok(()) +} + +/// Integer-only LERP with 8 bits of precision. +/// +/// Thanks to for the inspiration. +fn integer_lerp(a: u8, b: u8, f: u8) -> u8 { + let a = u16::from(a); + let b = u16::from(b); + let f = u16::from(f); + + let res = (a * (u16::from(u8::MAX) - f) + b * f) >> 8; + + res as u8 +} + +fn parallel_line_2_aa( + start: Point, + line_is_y_major: bool, + mut step: MajorMinor, + delta: MajorMinor, + c: Rgb888, + display: &mut impl DrawTarget, + extra: bool, + initial_error: i32, +) -> Result<(), std::convert::Infallible> { + let mut point = start; + + point += step.major * 2; + + let mut dx = delta.major; + let mut dy = delta.minor; + + if line_is_y_major { + dx *= 256; + point.x *= 256; + // step.minor.x = (step.minor.x * 256) / (step.minor.y * 256); + } else { + dy *= 256; + point.y *= 256; + // step.minor.y = (step.minor.y * 256) / (step.minor.x * 256); + } + + let e_minor = -2 * dx; + let e_major = 2 * dy; + let length = delta.major; + // Setting this to zero causes the first segment before the minor step to be too long + // let mut error = 2 * dy - dx; + let mut error = initial_error; + + let background = Rgb888::BLACK; + + for _i in 0..length { + let p = point; + + let p = Point::new( + if line_is_y_major { p.x >> 8 } else { p.x }, + if line_is_y_major { p.y } else { p.y >> 8 }, + ); + + let c = { + let mul = (if line_is_y_major { + point.x & 255 + } else { + point.y & 255 + }) as u8; + + // Some octants need the AA direction to go the other way + // let mul = if swap_aa_direction { 255 - mul } else { mul }; + + Rgb888::new( + integer_lerp(c.r(), background.r(), mul), + integer_lerp(c.g(), background.g(), mul), + integer_lerp(c.b(), background.b(), mul), + ) + }; + + Pixel(p, c).draw(display)?; + + // Draws a pixel connecting a diagonal move into a solid stairstep-looking piece. This is + // required for the additional diagonal move lines that are drawn when stepping in both the + // major and minor directions in the seed line. + if extra { + let p = point + step.minor; + + let p = Point::new( + if line_is_y_major { p.x >> 8 } else { p.x }, + if line_is_y_major { p.y } else { p.y >> 8 }, + ); + + Pixel(p, c).draw(display)?; + } + + if error > 0 { + point += step.minor; + error += e_minor; + } + + point += step.major; + error += e_major; + } + + Ok(()) +} + +fn parallel_line_2( + start: Point, + line_is_y_major: bool, + step: MajorMinor, + delta: MajorMinor, + c: Rgb888, + display: &mut impl DrawTarget, + extra: bool, + initial_error: i32, +) -> Result<(), std::convert::Infallible> { + let mut point = start; + + point += step.major * 2; + + let dx = delta.major; + let dy = delta.minor; + + let e_minor = -2 * dx; + let e_major = 2 * dy; + let length = dx; + // Setting this to zero causes the first segment before the minor step to be too long + // let mut error = 2 * dy - dx; + let mut error = initial_error; + + for _i in 0..length { + // let p = Point::new( + // if line_is_y_major { + // point.x >> 8 + // } else { + // point.x + // }, + // if line_is_y_major { + // point.y + // } else { + // point.y >> 8 + // }, + // ); + + let p = point; + + Pixel(p, c).draw(display)?; + + // Draws a pixel connecting a diagonal move into a solid stairstep-looking piece. This is + // required for the additional diagonal move lines that are drawn when stepping in both the + // major and minor directions in the seed line. + if extra { + let p = point + step.minor; + + let p = Point::new( + if line_is_y_major { p.x >> 8 } else { p.x }, + if line_is_y_major { p.y } else { p.y >> 8 }, + ); + + Pixel(p, c).draw(display)?; + } + + if error > 0 { + point += step.minor; + error += e_minor; + } + + point += step.major; + error += e_major; + } + + Ok(()) +} + +fn parallel_line_aa( + start: Point, + line_is_y_major: bool, + step: MajorMinor, + delta: MajorMinor, + c: Rgb888, + swap_aa_direction: bool, + display: &mut impl DrawTarget, +) -> Result<(), std::convert::Infallible> { + let mut point = start; + + let dx = delta.major; + let dy = delta.minor; + + let e_minor = -2 * dx; + let e_major = 2 * dy; + let length = dx; + let mut error = 2 * dy - dx; + + // Blend colour for AA edge + let background = Rgb888::BLACK; + + // FIXME: If line is exactly diagonal, no AA is performed. It should have a 50% edge. + + for _i in 0..length { + let aa_colour = { + let mul = (if line_is_y_major { + point.x & 255 + } else { + point.y & 255 + }) as u8; + + // Some octants need the AA direction to go the other way + let mul = if swap_aa_direction { 255 - mul } else { mul }; + + Rgb888::new( + integer_lerp(c.r(), background.r(), mul), + integer_lerp(c.g(), background.g(), mul), + integer_lerp(c.b(), background.b(), mul), + ) + }; + + let aa_p = Point::new( + if line_is_y_major { + point.x >> 8 + } else { + point.x + }, + if line_is_y_major { + point.y + } else { + point.y >> 8 + }, + ); + + Pixel(aa_p, aa_colour).draw(display)?; + + if error > 0 { + point += step.minor; + error += e_minor; + } + + error += e_major; + point += step.major; + } + + Ok(()) +} + +struct LineDebug { + start: Point, + end: Point, + stroke_width: u32, + phase: i32, + extra: bool, +} + +impl App for LineDebug { + type Color = Rgb888; + const DISPLAY_SIZE: Size = Size::new(200, 200); + // const DISPLAY_SIZE: Size = Size::new(64, 64); + + fn new() -> Self { + let end = Point::new( + Self::DISPLAY_SIZE.width as i32 / 2, + Self::DISPLAY_SIZE.height as i32 / 2, + ); + Self { + start: end - Point::new(80, 35), + end, + stroke_width: 10, + phase: 0, + extra: true, + } + } + + fn parameters(&mut self) -> Vec { + vec![ + Parameter::new("start", &mut self.start), + Parameter::new("end", &mut self.end), + Parameter::new("stroke", &mut self.stroke_width), + Parameter::new("phase", &mut self.phase), + Parameter::new("extra", &mut self.extra), + ] + } + + fn draw( + &self, + display: &mut SimulatorDisplay, + ) -> Result<(), std::convert::Infallible> { + let Point { x: _x0, y: _y0 } = self.start; + + // let width = 2 * self.stroke_width as i32 * f32::sqrt((dx * dx + dy * dy) as f32) as i32; + // let width = (self.stroke_width as i32).pow(2) * (dx * dx + dy * dy); + let width = self.stroke_width as i32; + + let _mock_display: MockDisplay = MockDisplay::new(); + + thickline( + display, + Line::new(self.start, self.end), + width, + self.extra, + self.phase, + )?; + + // let l = Line::new(self.start, self.end); + + // l.into_styled(PrimitiveStyle::with_stroke(Rgb888::GREEN, width as u32)) + // .draw(display)?; + + // l.perpendicular() + // .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)) + // .draw(&mut display.translated(Point::new(40, 40)))?; + + Ok(()) + } +} + +fn main() { + let settings = OutputSettingsBuilder::new().scale(5).build(); + let window = Window::new("Line debugger", &settings); + + LineDebug::run(window); +} diff --git a/framework/Cargo.toml b/framework/Cargo.toml index 7e3836f..41e786e 100644 --- a/framework/Cargo.toml +++ b/framework/Cargo.toml @@ -6,6 +6,6 @@ edition = "2018" publish = false [dependencies] -embedded-graphics = "0.7.0-beta.1" -embedded-graphics-simulator = "0.3.0-alpha.2" -sdl2 = "0.32.2" +embedded-graphics = "0.8.1" +embedded-graphics-simulator = { version = "0.6.0", path = "../../simulator" } +sdl2 = "0.35.1" diff --git a/framework/src/lib.rs b/framework/src/lib.rs index 918fb8b..fe0c02b 100644 --- a/framework/src/lib.rs +++ b/framework/src/lib.rs @@ -44,7 +44,10 @@ pub trait AppExt: App { fn run(window: Window); } -impl AppExt for T { +impl AppExt for T +where + ::Color: From, +{ fn run(mut window: Window) { let mut app = T::new(); let mut display = SimulatorDisplay::new(T::DISPLAY_SIZE);