diff --git a/Cargo.toml b/Cargo.toml index 7619b8b..088d505 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,18 +15,17 @@ repository = "https://github.com/shnewto/edges" license = "MIT OR Apache-2.0" [lints.clippy] -type_complexity = { level = "allow", priority = 1 } -needless_pass_by_value = { level = "allow", priority = 1 } cast_precision_loss = { level = "allow", priority = 1 } pedantic = { level = "warn", priority = 0 } [features] -default = ["glam-latest"] +default = ["bevy"] glam-latest = ["dep:glam"] bevy = ["dep:bevy_math", "dep:bevy_render"] [dependencies] image = "0.25" +rayon = "1.10.0" [dependencies.glam] version = "0.29" diff --git a/assets/diagonals.png b/assets/diagonals.png new file mode 100644 index 0000000..d630714 Binary files /dev/null and b/assets/diagonals.png differ diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index a24cba2..1af434b 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -32,25 +32,37 @@ fn main() { ) .unwrap(); - draw_png(boulders, "boulders"); - draw_png(more_lines, "more-lines"); + let diagonals = Image::from_buffer( + include_bytes!("../assets/diagonals.png"), + ImageType::Extension("png"), + CompressedImageFormats::default(), + true, + ImageSampler::default(), + RenderAssetUsages::default(), + ) + .unwrap(); + + draw_png(&boulders, "boulders"); + draw_png(&more_lines, "more-lines"); + draw_png(&diagonals, "diagonals"); } -fn draw_png(image: Image, img_path: &str) { +fn draw_png(image: &Image, img_path: &str) { + // get the image's edges + let edges = Edges::from(image); + 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, ); - // get the image's edges - let edges = Edges::from(image); // draw the edges to a png let mut dt = DrawTarget::new(width, height); - let objects_iter = edges.multi_image_edges_raw().into_iter(); + let objects = edges.multi_image_edges_raw(); - for object in objects_iter { + for object in objects { let mut pb = PathBuilder::new(); let mut edges_iter = object.into_iter(); diff --git a/examples/dynamic-image.rs b/examples/dynamic-image.rs index 6bd4db7..3e1e544 100644 --- a/examples/dynamic-image.rs +++ b/examples/dynamic-image.rs @@ -9,8 +9,10 @@ fn main() { } fn draw_png(img_path: &str) { - let image = &image::open(Path::new(&format!("assets/{img_path}"))).unwrap(); - let edges = Edges::from(image); + let image = image::open(Path::new(&format!("assets/{img_path}"))).unwrap(); + // get the image's edges + let edges = Edges::from(&image); + let scale = 8; let (width, height) = ( i32::try_from(image.width()).expect("Image to wide.") * scale, diff --git a/src/bin_image.rs b/src/bin_image.rs index 896a7e2..f2db458 100644 --- a/src/bin_image.rs +++ b/src/bin_image.rs @@ -1,4 +1,15 @@ -use crate::{UVec2, Vec2}; +use crate::{utils::is_corner, UVec2, Vec2}; +use rayon::prelude::*; +pub mod neighbors { + pub const NORTH: u8 = 0b1000_0000; + pub const SOUTH: u8 = 0b0100_0000; + pub const EAST: u8 = 0b0010_0000; + pub const WEST: u8 = 0b0001_0000; + pub const NORTHEAST: u8 = 0b0000_1000; + pub const NORTHWEST: u8 = 0b0000_0100; + pub const SOUTHEAST: u8 = 0b0000_0010; + pub const SOUTHWEST: u8 = 0b0000_0001; +} pub struct BinImage { data: Vec, @@ -27,10 +38,10 @@ impl BinImage { let compress_step = data.len() / (height * width) as usize; Self { data: data - .chunks(8 * compress_step) + .par_chunks(8 * compress_step) .map(|chunk| { chunk - .chunks(compress_step) + .par_chunks(compress_step) .map(|chunk| chunk.iter().any(|i| *i != 0)) .enumerate() .map(|(index, bit)| u8::from(bit) << index) @@ -52,15 +63,17 @@ 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 { - let (x, y) = (p.x, p.y); - let index = y * self.width + x; + 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 - x <= self.width && byte & 1 > 0 + byte & 1 > 0 } else { false } @@ -74,19 +87,39 @@ impl BinImage { /// /// # Returns /// - /// An array of 8 boolean values representing the state of the neighboring pixels. - pub fn get_neighbors(&self, p: UVec2) -> [bool; 8] { + /// An byte representing the state of the neighboring pixels. + pub fn get_neighbors(&self, p: UVec2) -> u8 { let (x, y) = (p.x, p.y); - [ - y < u32::MAX && self.get((x, y + 1).into()), // North - y > u32::MIN && self.get((x, y - 1).into()), // South - x < u32::MAX && self.get((x + 1, y).into()), // East - x > u32::MIN && self.get((x - 1, y).into()), // West - x < u32::MAX && y < u32::MAX && self.get((x + 1, y + 1).into()), // Northeast - x > u32::MIN && y > u32::MIN && self.get((x - 1, y - 1).into()), // Southwest - x < u32::MAX && y > u32::MIN && self.get((x + 1, y - 1).into()), // Southeast - x > u32::MIN && y < u32::MAX && self.get((x - 1, y + 1).into()), // Northwest - ] + let mut neighbors = 0; + if y < u32::MAX && self.get(UVec2::new(x, y + 1)) { + neighbors |= neighbors::NORTH; + } + if y > u32::MIN && self.get(UVec2::new(x, y - 1)) { + neighbors |= neighbors::SOUTH; + } + if x < u32::MAX && self.get(UVec2::new(x + 1, y)) { + neighbors |= neighbors::EAST; + } + if x > u32::MIN && self.get(UVec2::new(x - 1, y)) { + neighbors |= neighbors::WEST; + } + if x < u32::MAX && y < u32::MAX && self.get(UVec2::new(x + 1, y + 1)) { + neighbors |= neighbors::NORTHEAST; + } + if x > u32::MIN && y < u32::MAX && self.get(UVec2::new(x - 1, y + 1)) { + neighbors |= neighbors::NORTHWEST; + } + if x < u32::MAX && y > u32::MIN && self.get(UVec2::new(x + 1, y - 1)) { + neighbors |= neighbors::SOUTHEAST; + } + if x > u32::MIN && y > u32::MIN && self.get(UVec2::new(x - 1, y - 1)) { + neighbors |= neighbors::SOUTHWEST; + } + neighbors + } + + pub fn is_corner(&self, p: UVec2) -> bool { + is_corner(self.get_neighbors(p)) } /// Translates a point in positive (x, y) coordinates to a coordinate system centered at (0, 0). @@ -100,8 +133,8 @@ impl BinImage { /// A new `Vec2` representing the translated coordinates fn translate_point(&self, p: Vec2) -> Vec2 { Vec2::new( - p.x - (self.width as f32 / 2.0 - 1.0), - (self.height as f32 / 2.0 - 1.0) - p.y, + p.x - ((self.width / 2) as f32 - 1.0), + ((self.height / 2) as f32 - 1.0) - p.y, ) } @@ -115,7 +148,7 @@ impl BinImage { /// /// A vector of `Vec2` representing the translated coordinates. pub fn translate(&self, v: Vec) -> Vec { - v.into_iter().map(|p| self.translate_point(p)).collect() + v.into_par_iter().map(|p| self.translate_point(p)).collect() } pub const fn height(&self) -> u32 { diff --git a/src/lib.rs b/src/lib.rs index ec37732..6abfdca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,13 @@ #![doc = include_str!("../README.md")] +use crate::bin_image::BinImage; #[cfg(feature = "bevy")] pub use bevy_math::prelude::{UVec2, Vec2}; -#[cfg(not(feature = "bevy"))] +#[cfg(all(not(feature = "bevy"), feature = "glam-latest"))] pub use glam::{UVec2, Vec2}; +use rayon::prelude::*; use std::fmt; - -use crate::{bin_image::BinImage, utils::points_to_drawing_order}; +use utils::{handle_neighbors, in_polygon, Direction}; mod bin_image; #[cfg(feature = "bevy")] @@ -30,14 +31,14 @@ impl Edges { /// coordinates translated to either side of (0, 0) #[must_use] pub fn single_image_edge_translated(&self) -> Vec { - self.image_edges(true).into_iter().flatten().collect() + self.image_edges(true).into_par_iter().flatten().collect() } /// 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 #[must_use] pub fn single_image_edge_raw(&self) -> Vec { - self.image_edges(false).into_iter().flatten().collect() + self.image_edges(false).into_par_iter().flatten().collect() } /// If there's more than one sprite / object in the image, this returns all it finds, with @@ -62,24 +63,77 @@ impl Edges { // 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 // order, groups them into sets of connected pixels - let edge_points = (0..image.height() * image.width()) - .map(|i| (i / image.height(), i % image.height())) - .map(|(x, y)| UVec2::new(x, y)) - .filter(|p| image.get(*p)) - .filter(|p| (0..8).contains(&image.get_neighbors(*p).iter().filter(|i| **i).count())) + 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)) + .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 + } + } - points_to_drawing_order(edge_points) - .into_iter() - .map(|group| { - let group = group.into_iter().map(|p| p.as_vec2()).collect(); - if translate { - self.translate(group) - } else { - group + fn collect_objects(&self, corners: &[UVec2]) -> Vec> { + if corners.is_empty() { + return Vec::new(); + } + + let mut objects: Vec> = Vec::new(); + + while let Some(start) = corners.iter().find(|point| { + objects + .par_iter() + .all(|object| !(object.contains(point) || in_polygon(**point, object))) + }) { + let mut current = *start; + let mut group: Vec = Vec::new(); + group.push(current); + let object = loop { + let (last, neighbors) = (*group.last().unwrap(), self.image.get_neighbors(current)); + if last != current { + group.push(current); + } + match handle_neighbors(current, last, neighbors) { + Direction::North => current.y += 1, + Direction::South => current.y -= 1, + Direction::East => current.x += 1, + Direction::West => current.x -= 1, + Direction::Northeast => { + current.x += 1; + current.y += 1; + } + Direction::Northwest => { + current.x -= 1; + current.y += 1; + } + Direction::Southeast => { + current.x += 1; + current.y -= 1; + } + Direction::Southwest => { + current.x -= 1; + current.y -= 1; + } + } + if current == *start { + break group; } - }) - .collect() + }; + objects.push(object); + } + + objects } /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). @@ -127,9 +181,9 @@ impl fmt::Debug for Edges { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{}", + "Edges {{{}\n}}", format!( - "Edges {{\nraw: {:#?},\ntranslated: {:#?}\n}}", + "\nraw: {:#?},\ntranslated: {:#?}", self.image_edges(false), self.image_edges(true), ) diff --git a/src/utils.rs b/src/utils.rs index 07cd18d..c67a4f2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,69 +1,315 @@ -use crate::{UVec2, Vec2}; -use std::collections::HashMap; +use crate::UVec2; +use std::cmp::Ordering::{Equal, Greater, Less}; -// d=√((x2-x1)²+(y2-y1)²) -pub fn distance(a: Vec2, b: Vec2) -> f32 { - ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt() +// Get the bounding box of the polygon +fn bounding_box(polygon: &[UVec2]) -> Option<(UVec2, UVec2)> { + polygon + .iter() + .copied() + .zip(polygon.iter().copied()) + .reduce(|(min, max), (a, b)| (min.min(a), max.max(b))) } -/// Takes a collection of coordinates and attempts to sort them according to drawing order -/// -/// Pixel sorted so that the distance to previous and next is 1. When there is no pixel left -/// with distance 1, another group is created and sorted the same way. -pub fn points_to_drawing_order(points: Vec) -> Vec> { - if points.is_empty() { - return Vec::new(); +pub fn in_polygon(point: UVec2, polygon: &[UVec2]) -> bool { + if let Some((min, max)) = bounding_box(polygon) { + // Check if the point is within the bounding box + if point.x < min.x || point.x > max.x || point.y < min.y || point.y > max.y { + return false; // Early exit if outside the bounding box + } } - let mut groups: Vec> = Vec::new(); - let mut group: Vec = Vec::new(); - let mut drawn_points_with_counts = HashMap::new(); + let mut is_inside = false; - let mut start = points[0]; - let mut current = start; - group.push(current); - drawn_points_with_counts.insert(current, 2); + for i in 0..polygon.len() { + let (p1, p2) = (polygon[i], polygon[(i + 1) % polygon.len()]); + let (min, max) = (p1.min(p2), p1.max(p2)); + let (dy, dx) = (max.y - min.y, max.x - min.x); - while drawn_points_with_counts.len() < points.len() { - if let Some(p) = points - .iter() - .filter(|p| (distance(current.as_vec2(), p.as_vec2()) - 1.0).abs() <= f32::EPSILON) - .min_by_key(|n| drawn_points_with_counts.get(n).map_or(0, |c| *c)) - { - current = *p; - group.push(current); - if let Some(c) = drawn_points_with_counts.get_mut(p) { - *c += 1; - } else { - drawn_points_with_counts.insert(current, 2); + if min.y <= point.y && point.y < max.y && point.x <= min.x + dx * (point.y - min.y) / dy { + if min.x <= point.x && point.x < max.x { + return true; } + is_inside = !is_inside; } + } + is_inside +} - // we've traversed and backtracked and we're back at the start without reaching the end of the points - // so we need to start a collecting the points of a new unconnected object - if current == start { - // remove the connecting coordinate - let _ = group.pop(); - groups.push(group.clone()); - group.clear(); - for val in drawn_points_with_counts.values_mut() { - *val = 1; - } +pub fn is_corner(neighbors: u8) -> bool { + !matches!( + neighbors, + 255 + | 239 + | 238 + | 235 + | 234 + | 223 + | 221 + | 215 + | 213 + | 188..=207 + | 127 + | 123 + | 119 + | 115 + | 48..=63 + | 9 + | 6 + | 0 + ) +} + +pub enum Direction { + North, + South, + East, + West, - if let Some(new_start) = points - .iter() - .find(|p| !drawn_points_with_counts.contains_key(p)) - { - start = *new_start; - current = start; - group.push(current); - drawn_points_with_counts.insert(current, 2); - } else { - break; + Northeast, + Northwest, + Southeast, + Southwest, +} + +#[allow(clippy::too_many_lines)] +pub fn handle_neighbors(current: UVec2, last: UVec2, neighbors: u8) -> Direction { + use Direction::{East, North, Northeast, Northwest, South, Southeast, Southwest, West}; + match neighbors { + 0 | 255 => unreachable!(), + 188..=191 | 127 | 123 | 119 | 115 | 48..=63 => match last.x.cmp(¤t.x) { + Greater => West, + Equal => unreachable!(), + Less => East, + }, + 239 | 238 | 235 | 234 | 223 | 221 | 215 | 213 | 192..=207 => match last.y.cmp(¤t.y) { + Greater => South, + Equal => unreachable!(), + Less => North, + }, + 6 => match last.x.cmp(¤t.x) { + Greater => Northwest, + Equal => unreachable!(), + Less => Southeast, + }, + 9 => match last.x.cmp(¤t.x) { + Greater => Southwest, + Equal => unreachable!(), + Less => Northeast, + }, + + 140 | 136 | 132 | 128 => North, + 99 | 98 | 64..=67 => South, + 42 | 40 | 34 | 32 => East, + 21 | 20 | 17 | 16 => West, + 8 => Northeast, + 4 => Northwest, + 2 => Southeast, + 1 => Southwest, + 247 | 245 | 174 | 172 | 170 | 168 | 166 | 164 | 162 | 160 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => East, + Less => unreachable!(), + }, + 253 | 104..=107 | 97 | 96 => match last.x.cmp(¤t.x) { + Greater => South, + Equal => East, + Less => unreachable!(), + }, + 251 | 157 | 156 | 153 | 152 | 149 | 148 | 145 | 144 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => West, + Less => North, + }, + 254 | 250 | 80..=87 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => West, + Less => South, + }, + 180..=182 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => unreachable!(), + Less => East, + }, + 186 | 184 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => West, + Less => East, + }, + 231 | 226 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => South, + Less => unreachable!(), + }, + 236 | 232 => match last.y.cmp(¤t.y) { + Greater => South, + Equal => unreachable!(), + Less => East, + }, + 249 | 248 | 246 | 244 | 240..=242 => { + match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { + (Less, Equal) => South, + (Equal, Less) => East, + (Greater, Equal) => North, + (Equal, Greater) => West, + _ => unreachable!(), } } - } - groups.push(group); - groups + 110 | 103 | 102 => match last.x.cmp(¤t.x) { + Greater => Northwest, + Equal => unreachable!(), + Less => South, + }, + 111 | 109 | 108 | 101 | 100 => match last.x.cmp(¤t.x) { + Greater => Northwest, + Equal => East, + Less => South, + }, + 46 | 44 | 38 | 36 => match last.x.cmp(¤t.x) { + Greater => Northwest, + Equal => unreachable!(), + Less => East, + }, + 43 | 41 | 35 | 33 => match last.x.cmp(¤t.x) { + Greater => Southwest, + Equal => unreachable!(), + Less => East, + }, + 175 | 173 | 171 | 169 | 167 | 165 | 163 | 161 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => Southwest, + Less => East, + }, + 142 | 138 | 134 | 130 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => Southeast, + Less => unreachable!(), + }, + 95 | 93 | 91 | 89 => match last.x.cmp(¤t.x) { + Greater => West, + Equal => Northeast, + Less => unreachable!(), + }, + 141 | 137 | 133 | 129 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => Southwest, + Less => North, + }, + 94 | 92 | 90 | 88 => match last.x.cmp(¤t.x) { + Greater => West, + Equal => Northeast, + Less => South, + }, + 23 | 22 | 19 | 18 => match last.x.cmp(¤t.x) { + Greater => West, + Equal => unreachable!(), + Less => Southeast, + }, + 159 | 158 | 155 | 154 | 151 | 150 | 147 | 146 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => West, + Less => Southeast, + }, + 29 | 28 | 25 | 24 => match last.x.cmp(¤t.x) { + Greater => West, + Equal => unreachable!(), + Less => Northeast, + }, + 72..=75 => match last.x.cmp(¤t.x) { + Greater => South, + Equal => Northeast, + Less => unreachable!(), + }, + 68..=71 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => Northwest, + Less => South, + }, + + 31 | 30 | 27 | 26 => match last.y.cmp(¤t.y) { + Greater => West, + Equal => Southeast, + Less => Northeast, + }, + 76..=79 => match last.x.cmp(¤t.x) { + Greater => Northwest, + Equal => Northeast, + Less => South, + }, + 47 | 45 | 39 | 37 => match last.y.cmp(¤t.y) { + Greater => Southwest, + Equal => Northwest, + Less => East, + }, + 143 | 139 | 135 | 131 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => Southwest, + Less => Southeast, + }, + 10 => match last.y.cmp(¤t.y) { + Greater | Equal => Southeast, + Less => Northeast, + }, + 12 => match last.x.cmp(¤t.x) { + Greater => Northwest, + Equal => unreachable!(), + Less => Northeast, + }, + 3 => match last.x.cmp(¤t.x) { + Greater => Southwest, + Equal => unreachable!(), + Less => Southeast, + }, + 5 => match last.x.cmp(¤t.x) { + Greater => Southwest, + Equal => unreachable!(), + Less => Northwest, + }, + 15 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { + (Greater, Less) => Northeast, + (Greater, Greater) => Northwest, + (Less, Less) => Southeast, + (Less, Greater) => Southwest, + _ => unreachable!(), + }, + + 252 | 124..=126 | 120..=122 | 116..=118 | 112..=114 => match last.x.cmp(¤t.x) { + Greater => West, + Equal => East, + Less => South, + }, + 243 | 187 | 185 | 183 | 176..=179 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => West, + Less => East, + }, + 222 | 216..=220 | 214 | 208..=212 => match last.y.cmp(¤t.y) { + Greater => West, + Equal => South, + Less => North, + }, + 237 | 233 | 227..=230 | 225 | 224 => match last.y.cmp(¤t.y) { + Greater => South, + Equal => North, + Less => East, + }, + 7 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { + (Greater, Less) => Northwest, + (Less, Less) => Southeast, + (Less, Greater) => Southwest, + _ => unreachable!(), + }, + 14 | 11 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { + (Greater, Less) => Northeast, + (Less, Less) => Southeast, + (Greater, Greater) => Southwest, + _ => unreachable!(), + }, + 13 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { + (Less, Greater) => Northeast, + (Less, Less) => Southeast, + (Greater, Greater) => Southwest, + _ => unreachable!(), + }, + } }