diff --git a/Cargo.toml b/Cargo.toml index d209bdc..9120a09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ pedantic = { level = "warn", priority = 0 } [features] default = ["bevy"] glam-latest = ["dep:glam"] -bevy = ["dep:bevy_math", "dep:bevy_render"] +bevy = ["dep:bevy_math", "dep:bevy_image"] [dependencies] image = "0.25" @@ -32,19 +32,24 @@ version = "0.29" optional = true [dependencies.bevy_math] -version = "0.14" +version = "0.15" default-features = false optional = true -[dependencies.bevy_render] -version = "0.14" +[dependencies.bevy_image] +version = "0.15" default-features = false +features = ["png"] optional = true [dev-dependencies] raqote = "0.8" open = "5.1" +[dev-dependencies.bevy_render] +version = "0.15" +default-features = false + [[example]] name = "bevy-image" required-features = ["bevy"] diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index 1af434b..604a415 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -1,8 +1,5 @@ -use bevy_render::{ - prelude::Image, - render_asset::RenderAssetUsages, - texture::{CompressedImageFormats, ImageSampler, ImageType}, -}; +use bevy_image::{prelude::Image, CompressedImageFormats, ImageSampler, ImageType}; +use bevy_render::render_asset::RenderAssetUsages; use edges::Edges; use raqote::{DrawOptions, DrawTarget, PathBuilder, SolidSource, Source, StrokeStyle}; // in an actual bevy app, you wouldn't need all this building an Image from scratch logic, @@ -53,23 +50,23 @@ fn draw_png(image: &Image, img_path: &str) { let scale = 8; let (width, height) = ( - i32::try_from(image.width()).expect("Image to wide.") * scale, - i32::try_from(image.height()).expect("Image to tall.") * scale, + i32::try_from(image.width() * scale).expect("Image to wide."), + i32::try_from(image.height() * scale).expect("Image to tall."), ); // draw the edges to a png let mut dt = DrawTarget::new(width, height); - let objects = edges.multi_image_edges_raw(); + let objects = edges.multi_image_edge_raw(); for object in objects { let mut pb = PathBuilder::new(); let mut edges_iter = object.into_iter(); if let Some(first_edge) = edges_iter.next() { - pb.move_to(first_edge.x * scale as f32, first_edge.y * scale as f32); + pb.move_to((first_edge.x * scale) as f32, (first_edge.y * scale) as f32); for edge in edges_iter { - pb.line_to(edge.x * scale as f32, edge.y * scale as f32); + pb.line_to((edge.x * scale) as f32, (edge.y * scale) as f32); } } @@ -83,7 +80,7 @@ fn draw_png(image: &Image, img_path: &str) { a: 0xff, }), &StrokeStyle { - width: 1., + width: scale as f32, ..StrokeStyle::default() }, &DrawOptions::new(), diff --git a/examples/dynamic-image.rs b/examples/dynamic-image.rs index 3e1e544..803399c 100644 --- a/examples/dynamic-image.rs +++ b/examples/dynamic-image.rs @@ -15,8 +15,8 @@ fn draw_png(img_path: &str) { let scale = 8; let (width, height) = ( - i32::try_from(image.width()).expect("Image to wide.") * scale, - i32::try_from(image.height()).expect("Image to tall.") * scale, + i32::try_from(image.width() * scale).expect("Image to wide."), + i32::try_from(image.height() * scale).expect("Image to tall."), ); // draw the edges to a png @@ -25,9 +25,9 @@ fn draw_png(img_path: &str) { let mut edges_iter = edges.single_image_edge_raw().into_iter(); let first_edge = edges_iter.next().unwrap(); - pb.move_to(first_edge.x * scale as f32, first_edge.y * scale as f32); + pb.move_to((first_edge.x * scale) as f32, (first_edge.y * scale) as f32); for edge in edges_iter { - pb.line_to(edge.x * scale as f32, edge.y * scale as f32); + pb.line_to((edge.x * scale) as f32, (edge.y * scale) as f32); } let path = pb.finish(); @@ -40,7 +40,7 @@ fn draw_png(img_path: &str) { a: 0xff, }), &StrokeStyle { - width: 1., + width: scale as f32, ..StrokeStyle::default() }, &DrawOptions::new(), diff --git a/src/bin_image.rs b/src/bin_image.rs index f2db458..c7c2740 100644 --- a/src/bin_image.rs +++ b/src/bin_image.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use crate::{utils::is_corner, UVec2, Vec2}; use rayon::prelude::*; pub mod neighbors { @@ -11,6 +13,8 @@ pub mod neighbors { pub const SOUTHWEST: u8 = 0b0000_0001; } +/// A struct representing a binary image. +#[derive(Clone, Debug, Default)] pub struct BinImage { data: Vec, height: u32, @@ -31,7 +35,7 @@ impl BinImage { /// /// This function will panic if the length of `data` is less than `height * width`. pub fn new(height: u32, width: u32, data: &[u8]) -> Self { - assert!( + debug_assert!( data.len() >= (height * width) as usize, "data must not be smaller than image dimensions" ); @@ -64,27 +68,24 @@ impl BinImage { /// Returns `true` if the pixel is "on" (1), and `false` if it is "off" (0) or out of bounds. pub fn get(&self, p: UVec2) -> bool { if p.x >= self.width { - return false; - } - let index = p.y * self.width + p.x; - if let Some(mut byte) = self - .data - .get((index / 8) as usize) // index of byte - .copied() - { - byte >>= index % 8; // index of bit - byte & 1 > 0 - } else { false + } else { + let index = p.y * self.width + p.x; + if let Some(mut byte) = self + .data + .get((index / 8) as usize) // index of byte + .copied() + { + byte >>= index % 8; // index of bit + byte & 1 > 0 + } else { + false + } } } /// Gets the values of the neighboring pixels (8-connectivity) around the given coordinate. /// - /// # Arguments - /// - /// * `p` - A `UVec2` representing the coordinates of the center pixel. - /// /// # Returns /// /// An byte representing the state of the neighboring pixels. @@ -119,35 +120,27 @@ impl BinImage { } pub fn is_corner(&self, p: UVec2) -> bool { - is_corner(self.get_neighbors(p)) + self.get(p) && is_corner(self.get_neighbors(p)) } /// Translates a point in positive (x, y) coordinates to a coordinate system centered at (0, 0). /// - /// # Arguments - /// - /// * `p` - A `Vec2` representing the point to translate. - /// /// # Returns /// /// A new `Vec2` representing the translated coordinates - fn translate_point(&self, p: Vec2) -> Vec2 { + const fn translate_point(&self, p: UVec2) -> Vec2 { Vec2::new( - p.x - ((self.width / 2) as f32 - 1.0), - ((self.height / 2) as f32 - 1.0) - p.y, + p.x as f32 - (self.width / 2) as f32, + (self.height / 2) as f32 - p.y as f32, ) } /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). /// - /// # Arguments - /// - /// * `v` - An `Vec` of `Vec2` points to translate. - /// /// # Returns /// /// A vector of `Vec2` representing the translated coordinates. - pub fn translate(&self, v: Vec) -> Vec { + pub fn translate(&self, v: Vec) -> Vec { v.into_par_iter().map(|p| self.translate_point(p)).collect() } @@ -159,3 +152,19 @@ impl BinImage { self.width } } + +impl Display for BinImage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for y in 0..self.height() { + for x in 0..self.width() { + if self.get(UVec2::new(x, y)) { + write!(f, "█")?; + } else { + write!(f, "-")?; + } + } + writeln!(f)?; + } + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 6abfdca..cfe5d44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,11 +15,15 @@ mod bin_image; mod tests; mod utils; +/// A struct representing the edges of a image. +#[derive(Clone)] pub struct Edges { image: BinImage, } impl Edges { + /// Creates a new `Edges` instance from the given dimensions and pixel data. + #[inline] #[must_use] pub fn new(height: u32, width: u32, data: &[u8]) -> Self { Self { @@ -27,38 +31,54 @@ impl Edges { } } - /// If there's only one sprite / object in the image, this returns just one, with - /// coordinates translated to either side of (0, 0) + /// Translates the edges of a single image into a coordinate system centered at (0, 0). + /// + /// # Returns + /// + /// A vector of `Vec2` representing the translated edge points. + #[inline] #[must_use] pub fn single_image_edge_translated(&self) -> Vec { - self.image_edges(true).into_par_iter().flatten().collect() + self.translate(self.single_image_edge_raw()) } - /// If there's only one sprite / object in the image, this returns just one, with - /// coordinates left alone and all in positive x and y + /// Retrieves the raw edge points of a single image. + /// + /// # Returns + /// + /// A vector of `UVec2` representing the raw edge points. + #[inline] #[must_use] - pub fn single_image_edge_raw(&self) -> Vec { - self.image_edges(false).into_par_iter().flatten().collect() + pub fn single_image_edge_raw(&self) -> Vec { + self.image_edges().into_par_iter().flatten().collect() } - /// If there's more than one sprite / object in the image, this returns all it finds, with - /// coordinates translated to either side of (0, 0) + /// Translates the edges of multiple images into a coordinate system centered at (0, 0). + /// + /// # Returns + /// + /// A vector of vectors of `Vec2` representing the translated edge points of each image. + #[inline] #[must_use] pub fn multi_image_edge_translated(&self) -> Vec> { - self.image_edges(true) + self.translate_objects(self.multi_image_edge_raw()) } - /// If there's more than one sprite / object in the image, this returns all it finds, with - /// coordinates left alone and all in positive x and y + /// Retrieves the raw edge points of multiple images. + /// + /// # Returns + /// + /// A vector of vectors of `UVec2` representing the raw edge points of each image. + #[inline] #[must_use] - pub fn multi_image_edges_raw(&self) -> Vec> { - self.image_edges(false) + pub fn multi_image_edge_raw(&self) -> Vec> { + self.image_edges() } /// Takes `Edges` and a boolean to indicate whether to translate /// the points you get back to either side of (0, 0) instead of everything in positive x and y. #[must_use] - pub fn image_edges(&self, translate: bool) -> Vec> { + pub fn image_edges(&self) -> Vec> { let image = &self.image; // Marching squares adjacent, walks all the pixels in the provided data and keeps track of // any that have at least one transparent / zero value neighbor then, while sorting into drawing @@ -66,22 +86,10 @@ impl Edges { let corners: Vec<_> = (0..image.height() * image.width()) .into_par_iter() .map(|i| UVec2::new(i / image.height(), i % image.height())) - .filter(|p| image.get(*p) && image.is_corner(*p)) + .filter(|p| image.is_corner(*p)) .collect(); - let objects: Vec<_> = self - .collect_objects(&corners) - .into_par_iter() - .map(|object| object.into_par_iter().map(|p| p.as_vec2()).collect()) - .collect(); - if translate { - objects - .into_par_iter() - .map(|object| self.translate(object)) - .collect() - } else { - objects - } + self.collect_objects(&corners) } fn collect_objects(&self, corners: &[UVec2]) -> Vec> { @@ -138,22 +146,38 @@ impl Edges { /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). /// - /// # Arguments - /// - /// * `v` - An `Vec` of `Vec2` points to translate. - /// /// # Returns /// /// A vector of `Vec2` representing the translated coordinates. + #[inline] #[must_use] - pub fn translate(&self, v: Vec) -> Vec { + pub fn translate(&self, v: Vec) -> Vec { self.image.translate(v) } + + /// Translates an `Vec` of `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). + /// + /// # Returns + /// + /// A vector of vector of `Vec2` representing the translated objects. + #[inline] + #[must_use] + pub fn translate_objects(&self, v: Vec>) -> Vec> { + v.into_par_iter() + .map(|v| self.translate(v)) + .collect::>() + } +} + +impl From for Vec> { + fn from(value: Edges) -> Vec> { + value.image_edges() + } } #[cfg(feature = "bevy")] -impl From for Edges { - fn from(i: bevy_render::prelude::Image) -> Edges { +impl From for Edges { + fn from(i: bevy_image::prelude::Image) -> Edges { Self::new(i.height(), i.width(), &i.data) } } @@ -165,8 +189,8 @@ impl From for Edges { } #[cfg(feature = "bevy")] -impl From<&bevy_render::prelude::Image> for Edges { - fn from(i: &bevy_render::prelude::Image) -> Edges { +impl From<&bevy_image::prelude::Image> for Edges { + fn from(i: &bevy_image::prelude::Image) -> Edges { Self::new(i.height(), i.width(), &i.data) } } @@ -179,15 +203,9 @@ impl From<&image::DynamicImage> for Edges { impl fmt::Debug for Edges { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Edges {{{}\n}}", - format!( - "\nraw: {:#?},\ntranslated: {:#?}", - self.image_edges(false), - self.image_edges(true), - ) - .replace('\n', "\n "), - ) + f.debug_struct("Edges") + .field("raw", &self.image_edges()) + .field("translated", &self.translate_objects(self.image_edges())) + .finish() } } diff --git a/src/tests.rs b/src/tests.rs index 4611555..49f3520 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,8 +1,6 @@ use crate::Edges; -use bevy_render::{ - render_asset::RenderAssetUsages, - texture::{CompressedImageFormats, Image, ImageSampler, ImageType}, -}; +use bevy_image::{prelude::Image, CompressedImageFormats, ImageSampler, ImageType}; +use bevy_render::render_asset::RenderAssetUsages; use std::path::Path; #[test] @@ -14,7 +12,7 @@ fn same_image_same_edges() { include_bytes!("../assets/car.png"), // buffer ImageType::Extension("png"), CompressedImageFormats::default(), - true, // + true, ImageSampler::default(), RenderAssetUsages::default(), ) @@ -40,7 +38,7 @@ fn same_images_same_edges() { include_bytes!("../assets/boulders.png"), // buffer ImageType::Extension("png"), CompressedImageFormats::default(), - true, // + true, ImageSampler::default(), RenderAssetUsages::default(), ) @@ -48,8 +46,8 @@ fn same_images_same_edges() { let bevy_edges = Edges::from(bevy_image); assert_eq!( - dynamic_edges.multi_image_edges_raw(), - bevy_edges.multi_image_edges_raw() + dynamic_edges.multi_image_edge_raw(), + bevy_edges.multi_image_edge_raw() ); assert_eq!( dynamic_edges.multi_image_edge_translated(),