From 1f33acbcc5afb6cff63534aff1893813cda876b8 Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 20 Oct 2024 16:24:47 +0900 Subject: [PATCH 01/32] fix: lsp warnings --- README.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4c3f00f..90a373a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -[![Crates.io]()]() -[![Crates.io]()]() -[![MIT/Apache 2.0]()]() - # edges +[![Crates.io](https://img.shields.io/crates/v/edges.svg)](https://crates.io/crates/edges) +[![Crates.io](https://img.shields.io/crates/d/edges.svg)](https://crates.io/crates/edges) +[![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/shnewto/edges#license) + get the edges of objects in images with transparency. ## supported image types @@ -24,13 +24,20 @@ println!("{:#?}", edges.single_image_edge_translated()); ## how it works -i was inspired by [a coding train (or, coding in the cabana rather) on an implementation of "marching squares"](). -so this crate takes a "march through all the values" approach to find edges, i.e. pixels with at least 1 empty neighboring pixel, but -instead of drawing a contour in place, it just keeps track of all the actual pixel coordinates. to determine "empty" I bitwise -or all the bytes for each pixel and, in images with transparency, "empty" is a zero value for the pixel. - -after that, we need to put the coordinates in some kind of "drawing order" so whatever we pass all the points to, knows how we want the object constructed. for this, the -crate collects all pixels, in order, that are a distance of 1 from eachother. if there are pixels that have a distance greater than 1 +i was inspired by [a coding train (or, coding in the cabana rather) +on an implementation of "marching squares"](https://youtu.be/0ZONMNUKTfU). +so this crate takes a "march through all the values" approach to find edges, i.e. +pixels with at least 1 empty neighboring pixel, but +instead of drawing a contour in place, +it just keeps track of all the actual pixel coordinates. to determine "empty" I bitwise +or all the bytes for each pixel and, +in images with transparency, "empty" is a zero value for the pixel. + +after that, we need to put the coordinates in some kind of +"drawing order" so whatever we pass all the points to, +knows how we want the object constructed. for this, the +crate collects all pixels, in order, that are a distance of 1 from eachother. +if there are pixels that have a distance greater than 1 from any pixel in an existing group, that pixel begins a new group. ## license From 8f953d6c19686302e9cb131ff9140b41371a3703 Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 20 Oct 2024 16:25:32 +0900 Subject: [PATCH 02/32] upd: deps and add lint params --- Cargo.toml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b50fd90..f1fa155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,15 +14,21 @@ 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=[] -bevy=["dep:bevy"] +default = [] +bevy = ["dep:bevy"] [dependencies] -glam = "0.27.0" -hashbrown = "0.14" +glam = "0.29.0" +hashbrown = "0.15" image = "0.25" -mashmap = "0.1" +mashmap = "0.2.1" ordered-float = "4.2" thiserror = "1.0" From 91f4b86b863df8bb7e801aff26b45f53b0a77621 Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 20 Oct 2024 21:07:06 +0900 Subject: [PATCH 03/32] fix: lsp warnings --- examples/bevy-image.rs | 31 ++++++++++++++++++++----------- examples/dynamic-image.rs | 13 ++++++++----- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index 7dd6852..c226c59 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -1,6 +1,12 @@ -use bevy::{prelude::Image, render::texture::ImageType}; +use bevy::{ + prelude::Image, + render::{ + render_asset::RenderAssetUsages, + texture::{CompressedImageFormats, ImageSampler, ImageType}, + }, +}; use edges::Edges; -use raqote::*; +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, // it'd be something closer to this: // `let image = image_assets.get(handle).unwrap();` @@ -11,20 +17,20 @@ fn main() { let boulders = Image::from_buffer( include_bytes!("../assets/boulders.png"), ImageType::Extension("png"), - Default::default(), + CompressedImageFormats::default(), true, - Default::default(), - Default::default(), + ImageSampler::default(), + RenderAssetUsages::default(), ) .unwrap(); let more_lines = Image::from_buffer( include_bytes!("../assets/more-lines.png"), ImageType::Extension("png"), - Default::default(), + CompressedImageFormats::default(), true, - Default::default(), - Default::default(), + ImageSampler::default(), + RenderAssetUsages::default(), ) .unwrap(); @@ -36,7 +42,10 @@ fn draw_png(image: Image, img_path: &str) { // get the image's edges let edges = Edges::from(image.clone()); let scale = 8; - let (width, height) = (image.width() as i32 * scale, image.height() as i32 * scale); + let (width, height) = ( + i32::try_from(image.width()).expect("Image to wide.") * scale, + i32::try_from(image.height()).expect("Image to tall.") * scale, + ); // draw the edges to a png let mut dt = DrawTarget::new(width, height); @@ -71,6 +80,6 @@ fn draw_png(image: Image, img_path: &str) { ); } - dt.write_png(format!("edges-{}", img_path)).unwrap(); - _ = open::that(format!("edges-{}", img_path)); + dt.write_png(format!("edges-{img_path}.png")).unwrap(); + _ = open::that(format!("edges-{img_path}.png")); } diff --git a/examples/dynamic-image.rs b/examples/dynamic-image.rs index 5c1cb1d..6bd4db7 100644 --- a/examples/dynamic-image.rs +++ b/examples/dynamic-image.rs @@ -1,5 +1,5 @@ use edges::Edges; -use raqote::*; +use raqote::{DrawOptions, DrawTarget, PathBuilder, SolidSource, Source, StrokeStyle}; use std::path::Path; fn main() { @@ -9,10 +9,13 @@ fn main() { } fn draw_png(img_path: &str) { - let image = &image::open(Path::new(&format!("assets/{}", img_path))).unwrap(); + let image = &image::open(Path::new(&format!("assets/{img_path}"))).unwrap(); let edges = Edges::from(image); let scale = 8; - let (width, height) = (image.width() as i32 * scale, image.height() as i32 * scale); + let (width, height) = ( + i32::try_from(image.width()).expect("Image to wide.") * scale, + i32::try_from(image.height()).expect("Image to tall.") * scale, + ); // draw the edges to a png let mut dt = DrawTarget::new(width, height); @@ -41,6 +44,6 @@ fn draw_png(img_path: &str) { &DrawOptions::new(), ); - dt.write_png(format!("edges-{}", img_path)).unwrap(); - _ = open::that(format!("edges-{}", img_path)); + dt.write_png(format!("edges-{img_path}")).unwrap(); + _ = open::that(format!("edges-{img_path}")); } From e7ab40a25e933bce24380bc090dea503b0bc93d4 Mon Sep 17 00:00:00 2001 From: salam Date: Mon, 21 Oct 2024 12:16:10 +0900 Subject: [PATCH 04/32] rework dependencies --- Cargo.toml | 20 ++++++++++++++------ examples/bevy-image.rs | 12 +++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f1fa155..6afeb70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,21 +21,29 @@ cast_precision_loss = { level = "allow", priority = 1 } pedantic = { level = "warn", priority = 0 } [features] -default = [] -bevy = ["dep:bevy"] +default = ["glam-latest"] +glam-latest = ["dep:glam"] +bevy = ["dep:bevy_math", "dep:bevy_render"] [dependencies] -glam = "0.29.0" hashbrown = "0.15" image = "0.25" -mashmap = "0.2.1" +mashmap = "0.2" ordered-float = "4.2" thiserror = "1.0" -[dependencies.bevy] +[dependencies.glam] +version = "0.29" +optional = true + +[dependencies.bevy_math] +version = "0.14" +default-features = false +optional = true + +[dependencies.bevy_render] version = "0.14" default-features = false -features = ["bevy_render"] optional = true [dev-dependencies] diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index c226c59..19aa94e 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -1,9 +1,7 @@ -use bevy::{ +use bevy_render::{ prelude::Image, - render::{ - render_asset::RenderAssetUsages, - texture::{CompressedImageFormats, ImageSampler, ImageType}, - }, + render_asset::RenderAssetUsages, + texture::{CompressedImageFormats, ImageSampler, ImageType}, }; use edges::Edges; use raqote::{DrawOptions, DrawTarget, PathBuilder, SolidSource, Source, StrokeStyle}; @@ -34,8 +32,8 @@ fn main() { ) .unwrap(); - draw_png(boulders, "boulders.png"); - draw_png(more_lines, "more-lines.png"); + draw_png(boulders, "boulders"); + draw_png(more_lines, "more-lines"); } fn draw_png(image: Image, img_path: &str) { From 55dbda4de10cd2826a771163b020fed93c665993 Mon Sep 17 00:00:00 2001 From: salam Date: Mon, 21 Oct 2024 12:17:24 +0900 Subject: [PATCH 05/32] fix features --- src/edges.rs | 5 ++++- src/lib.rs | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/edges.rs b/src/edges.rs index b64c674..5b2820c 100644 --- a/src/edges.rs +++ b/src/edges.rs @@ -1,6 +1,9 @@ +#[cfg(feature = "bevy")] +pub use bevy_math::prelude::Vec2; +#[cfg(not(feature = "bevy"))] +pub use glam::Vec2; use std::fmt; -use glam::Vec2; use hashbrown::HashSet; use mashmap::MashMap; use ordered_float::OrderedFloat; diff --git a/src/lib.rs b/src/lib.rs index 0771ee0..c9bb25b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,3 @@ mod edges; mod error; pub use edges::*; -pub use glam::Vec2; From 0dbe7d8d345d040ab4eb2be40c7ca317b96d54aa Mon Sep 17 00:00:00 2001 From: salam Date: Mon, 21 Oct 2024 12:28:48 +0900 Subject: [PATCH 06/32] fix lsp warnings and tests --- src/edges.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/edges.rs b/src/edges.rs index 5b2820c..2bba6b4 100644 --- a/src/edges.rs +++ b/src/edges.rs @@ -17,30 +17,35 @@ pub enum Edges { 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) + #[must_use] pub fn single_image_edge_translated(&self) -> Vec { self.image_edges(true).into_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() } /// 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) + #[must_use] pub fn multi_image_edge_translated(&self) -> Vec> { self.image_edges(true) } /// 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 + #[must_use] pub fn multi_image_edges_raw(&self) -> Vec> { self.image_edges(false) } /// Takes a Bevy DynamicImage type and an 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> { let rows = self.height(); let cols = self.width(); @@ -287,7 +292,7 @@ impl fmt::Debug for Edges { raw: self.image_edges(false), translated: self.image_edges(true), }; - write!(f, "{:#?}", edges_display) + write!(f, "{edges_display:#?}") } } @@ -295,7 +300,10 @@ impl fmt::Debug for Edges { #[cfg(test)] mod tests { use crate::Edges; - use bevy::render::texture::{Image, ImageType}; + use bevy_render::{ + render_asset::RenderAssetUsages, + texture::{CompressedImageFormats, Image, ImageSampler, ImageType}, + }; use std::path::Path; #[test] @@ -306,10 +314,10 @@ mod tests { let bevy_image = Image::from_buffer( include_bytes!("../assets/car.png"), // buffer ImageType::Extension("png"), - Default::default(), + CompressedImageFormats::default(), true, // - Default::default(), - Default::default(), + ImageSampler::default(), + RenderAssetUsages::default(), ) .unwrap(); let bevy_edges = Edges::from(bevy_image); @@ -332,10 +340,10 @@ mod tests { let bevy_image = Image::from_buffer( include_bytes!("../assets/boulders.png"), // buffer ImageType::Extension("png"), - Default::default(), + CompressedImageFormats::default(), true, // - Default::default(), - Default::default(), + ImageSampler::default(), + RenderAssetUsages::default(), ) .unwrap(); let bevy_edges = Edges::from(bevy_image); From 3f5052fbe720eee8011e26e617b737f4577a28d7 Mon Sep 17 00:00:00 2001 From: salam Date: Mon, 21 Oct 2024 12:49:27 +0900 Subject: [PATCH 07/32] rework of Edges --- src/edges.rs | 236 +++++++++++++++++++++------------------------------ 1 file changed, 96 insertions(+), 140 deletions(-) diff --git a/src/edges.rs b/src/edges.rs index 2bba6b4..7450951 100644 --- a/src/edges.rs +++ b/src/edges.rs @@ -8,13 +8,43 @@ use hashbrown::HashSet; use mashmap::MashMap; use ordered_float::OrderedFloat; -pub enum Edges { - DynamicImage(image::DynamicImage), - #[cfg(feature = "bevy")] - BevyImage(bevy::prelude::Image), +pub struct Edges { + data: Vec, + height: usize, + width: usize, } impl Edges { + #[must_use] + pub fn new(height: usize, width: usize, data: &[u8]) -> Self { + let new = Self { + height, + width, + data: { + let binary = if data.len() > (height * width) { + let mut binary = Vec::with_capacity(height * width); + let compress_step = data.len() / (height * width); + for i in (0..data.len()).step_by(compress_step) { + binary.push(data[i..i + compress_step].iter().any(|i| *i > 0)); + } + binary + } else { + data.iter().map(|i| *i > 0).collect() + }; + let mut compressed = Vec::with_capacity(height * width / 8); + for i in (0..binary.len()).step_by(8) { + let mut byte = 0; + for bit in &binary[i..i + 7] { + byte += u8::from(*bit); + byte <<= 1; + } + compressed.push(byte); + } + compressed + }, + }; + new + } /// If there's only one sprite / object in the image, this returns just one, with /// coordinates translated to either side of (0, 0) #[must_use] @@ -43,95 +73,59 @@ impl Edges { self.image_edges(false) } - /// Takes a Bevy DynamicImage type and an 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 + /// 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> { - let rows = self.height(); - let cols = self.width(); - let data: &[u8] = self.bytes(); - let mut byte_combine_step: usize = 1; - if (rows * cols) < data.len() { - byte_combine_step = data.len() / (rows * cols); - } - - let mut processed: Vec = vec![]; - for i in (0..data.len()).step_by(byte_combine_step) { - let mut b: usize = 0; - for j in 0..byte_combine_step { - b |= data[i + j] as usize; // just need to retain any non-zero values - } - processed.push(b); - } - - Edges::march_edges(&processed, rows, cols, translate) - } - - /// 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 - /// - /// Accepts a flag indicating whether or not to translate coordinates to either side of (0,0) - /// or leave it all in positive x,y - pub fn march_edges( - data: &[usize], - rows: usize, - cols: usize, - translate: bool, - ) -> Vec> { - let mut edge_points: Vec = vec![]; - - for d in 0..data.len() { - let (x, y) = Edges::get_xy(d, rows); - let (c, r) = (x as isize, y as isize); - - if Edges::get_at(r, c, rows, cols, data) == 0 { + let mut edge_points: Vec = Vec::new(); + // 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 + for i in 0..self.width * self.height { + let (x, y) = self.get_pos(i); + if !self.get(x, y) { continue; } - let neighbors = [ - Edges::get_at(r + 1, c, rows, cols, data), - Edges::get_at(r - 1, c, rows, cols, data), - Edges::get_at(r, c + 1, rows, cols, data), - Edges::get_at(r, c - 1, rows, cols, data), - Edges::get_at(r + 1, c + 1, rows, cols, data), - Edges::get_at(r - 1, c - 1, rows, cols, data), - Edges::get_at(r + 1, c - 1, rows, cols, data), - Edges::get_at(r - 1, c + 1, rows, cols, data), + y < usize::MAX && self.get(x, y + 1), + y > usize::MIN && self.get(x, y - 1), + x < usize::MAX && self.get(x + 1, y), + x > usize::MIN && self.get(x - 1, y), + x < usize::MAX && y < usize::MAX && self.get(x + 1, y + 1), + x > usize::MIN && y > usize::MIN && self.get(x - 1, y - 1), + x < usize::MAX && y > usize::MIN && self.get(x + 1, y - 1), + x > usize::MIN && y < usize::MAX && self.get(x - 1, y + 1), ]; - - let n: usize = neighbors.iter().sum(); - let surrounded = neighbors.len(); - if n < surrounded { - edge_points.push(Vec2::new(x, y)); + if neighbors.iter().filter(|i| **i).count() < neighbors.len() { + edge_points.push(Vec2::new(x as f32, y as f32)); } } - Edges::points_to_drawing_order(&edge_points, translate, rows, cols) + self.points_to_drawing_order(edge_points, translate) } /// 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. - fn points_to_drawing_order( - points: &[Vec2], - translate: bool, - rows: usize, - cols: usize, - ) -> Vec> { - let mut groups: Vec> = vec![]; - let mut in_drawing_order: Vec = vec![]; + fn points_to_drawing_order(&self, points: Vec, translate: bool) -> Vec> { + let mut groups: Vec> = Vec::new(); + if points.is_empty() { + return groups; + } + + let mut in_drawing_order: Vec = Vec::new(); let mut drawn_points_with_counts: MashMap<(OrderedFloat, OrderedFloat), ()> = MashMap::new(); let mut drawn_points: HashSet<(OrderedFloat, OrderedFloat)> = HashSet::new(); let hashable = |v: Vec2| (OrderedFloat(v.x), OrderedFloat(v.y)); - if points.is_empty() { - return groups; - } + // d=√((x2-x1)²+(y2-y1)²) + let distance = |a: Vec2, b: Vec2| -> f32 { + ((a.x - b.x).abs().powi(2) + (a.y - b.y).abs().powi(2)).sqrt() + }; - let mut current = points[0]; - let mut start = current; + let mut start = points[0]; + let mut current = start; in_drawing_order.push(current); drawn_points_with_counts.insert(hashable(current), ()); drawn_points.insert(hashable(current)); @@ -139,7 +133,7 @@ impl Edges { while drawn_points.len() < points.len() { let neighbors = &points .iter() - .filter(|p| Edges::distance(current, **p) == 1.0) + .filter(|p| (distance(current, **p) - 1.0).abs() < 0.000_000_1) .collect::>(); if let Some(p) = neighbors @@ -179,103 +173,65 @@ impl Edges { groups.push(in_drawing_order.clone()); if translate { - groups = groups - .into_iter() - .map(|p| Edges::translate_vec(p, rows, cols)) - .collect(); + groups = groups.into_iter().map(|p| self.translate_vec(p)).collect(); } groups } /// conceptual helper, access a 1D vector like it's a 2D vector - fn get_xy(idx: usize, offset: usize) -> (f32, f32) { - let quot = idx / offset; - let rem = idx % offset; - (quot as f32, rem as f32) - } - - /// pythagoras, distance between two points - fn distance(a: Vec2, b: Vec2) -> f32 { - // d=√((x2-x1)²+(y2-y1)²) - ((a.x - b.x).abs().powi(2) + (a.y - b.y).abs().powi(2)).sqrt() + fn get_pos(&self, index: usize) -> (usize, usize) { + let quot = index / self.height; + let rem = index % self.height; + (quot, rem) } /// get zero or non-zero pixel the value at given coordinate - fn get_at(row: isize, col: isize, rows: usize, cols: usize, data: &[usize]) -> usize { - if row < 0 || col < 0 || row >= rows as isize || col >= cols as isize { - 0 + fn get(&self, x: usize, y: usize) -> bool { + let index = y * self.width / 8 + x / 8; + if let Some(mut byte) = self.data.get(index).copied() { + byte >>= 7 - x % 8; + byte & 1 > 0 } else { - let idx = row as usize * cols + col as usize; - data.get(idx) - .map(|i| if *i == 0 { 0 } else { 1 }) - .unwrap_or_else(|| 0) + false } } /// translate point in positive x,y to either side of (0,0) - fn xy_translate(p: Vec2, rows: usize, cols: usize) -> Vec2 { + fn translate_point(&self, p: Vec2) -> Vec2 { Vec2::new( - p.x - (cols as f32 / 2. - 1.0), - -p.y + (rows as f32 / 2. - 1.0), + p.x - (self.width as f32 / 2.0 - 1.0), + (self.height as f32 / 2.0 - 1.0) - p.y, ) } /// Translate vector of points in positive x,y to either side of (0,0) - pub fn translate_vec(v: Vec, rows: usize, cols: usize) -> Vec { - v.into_iter() - .map(|p| Edges::xy_translate(p, rows, cols)) - .collect() - } - - fn width(&self) -> usize { - match self { - Edges::DynamicImage(i) => i.width() as usize, - #[cfg(feature = "bevy")] - Edges::BevyImage(i) => i.size().x as usize, - } - } - - fn height(&self) -> usize { - match self { - Edges::DynamicImage(i) => i.height() as usize, - #[cfg(feature = "bevy")] - Edges::BevyImage(i) => i.size().y as usize, - } - } - - fn bytes(&self) -> &[u8] { - match self { - Edges::DynamicImage(i) => i.as_bytes(), - #[cfg(feature = "bevy")] - Edges::BevyImage(i) => &i.data, - } - } -} - -#[cfg(feature = "bevy")] -impl From for Edges { - fn from(i: bevy::prelude::Image) -> Edges { - Edges::BevyImage(i) + #[must_use] + pub fn translate_vec(&self, v: Vec) -> Vec { + v.into_iter().map(|p| self.translate_point(p)).collect() } } #[cfg(feature = "bevy")] -impl From<&bevy::prelude::Image> for Edges { - fn from(i: &bevy::prelude::Image) -> Edges { - Edges::BevyImage(i.clone()) +impl From for Edges { + fn from(i: bevy_render::prelude::Image) -> Edges { + Self::new(i.height() as usize, i.width() as usize, &i.data) } } impl From for Edges { fn from(i: image::DynamicImage) -> Edges { - Edges::DynamicImage(i) + Self::new(i.height() as usize, i.width() as usize, i.as_bytes()) } } -impl From<&image::DynamicImage> for Edges { - fn from(i: &image::DynamicImage) -> Edges { - Edges::DynamicImage(i.clone()) +impl From<&T> for Edges +where + T: Clone, + Edges: From, +{ + fn from(value: &T) -> Self { + Self::from(value.clone()) } } From 51976624ede4a744a298002088549fa7fdcfc646 Mon Sep 17 00:00:00 2001 From: salam Date: Tue, 22 Oct 2024 01:31:42 +0900 Subject: [PATCH 08/32] removed image cloning --- examples/bevy-image.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index 19aa94e..a24cba2 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -37,13 +37,13 @@ fn main() { } fn draw_png(image: Image, img_path: &str) { - // get the image's edges - let edges = Edges::from(image.clone()); 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); From d920c5f0c90b4d00bc497d26f68097517882e107 Mon Sep 17 00:00:00 2001 From: salam Date: Tue, 22 Oct 2024 01:32:22 +0900 Subject: [PATCH 09/32] remove unused struct --- src/error.rs | 2 -- src/lib.rs | 1 - 2 files changed, 3 deletions(-) delete mode 100644 src/error.rs diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 43779ae..0000000 --- a/src/error.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[derive(thiserror::Error, Debug)] -pub enum Error {} diff --git a/src/lib.rs b/src/lib.rs index c9bb25b..26fd67b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ #![doc = include_str!("../README.md")] mod edges; -mod error; pub use edges::*; From a9f37e0140ddb8360915b9cc5394e5bb537b4f76 Mon Sep 17 00:00:00 2001 From: salam Date: Tue, 22 Oct 2024 01:36:06 +0900 Subject: [PATCH 10/32] fixed: image compression --- Cargo.toml | 1 - src/edges.rs | 110 +++++++++++++++++++++++++-------------------------- 2 files changed, 53 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6afeb70..62b1e3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ bevy = ["dep:bevy_math", "dep:bevy_render"] hashbrown = "0.15" image = "0.25" mashmap = "0.2" -ordered-float = "4.2" thiserror = "1.0" [dependencies.glam] diff --git a/src/edges.rs b/src/edges.rs index 7450951..08ca326 100644 --- a/src/edges.rs +++ b/src/edges.rs @@ -6,7 +6,8 @@ use std::fmt; use hashbrown::HashSet; use mashmap::MashMap; -use ordered_float::OrderedFloat; + +type Point = (usize, usize); pub struct Edges { data: Vec, @@ -17,33 +18,24 @@ pub struct Edges { impl Edges { #[must_use] pub fn new(height: usize, width: usize, data: &[u8]) -> Self { - let new = Self { + Self { height, width, data: { - let binary = if data.len() > (height * width) { - let mut binary = Vec::with_capacity(height * width); - let compress_step = data.len() / (height * width); - for i in (0..data.len()).step_by(compress_step) { - binary.push(data[i..i + compress_step].iter().any(|i| *i > 0)); - } - binary - } else { - data.iter().map(|i| *i > 0).collect() - }; - let mut compressed = Vec::with_capacity(height * width / 8); - for i in (0..binary.len()).step_by(8) { - let mut byte = 0; - for bit in &binary[i..i + 7] { - byte += u8::from(*bit); - byte <<= 1; - } - compressed.push(byte); - } - compressed + let compress_step = data.len() / (height * width); + data.chunks(compress_step) + .map(|chunk| chunk.iter().any(|i| *i != 0)) + .collect::>() + .chunks(8) + .map(|byte| { + byte.iter() + .enumerate() + .map(|(i, bit)| u8::from(*bit) << i) + .sum::() + }) + .collect() }, - }; - new + } } /// If there's only one sprite / object in the image, this returns just one, with /// coordinates translated to either side of (0, 0) @@ -77,7 +69,7 @@ impl Edges { /// 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> { - let mut edge_points: Vec = Vec::new(); + let mut edge_points = Vec::new(); // 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 @@ -97,7 +89,7 @@ impl Edges { x > usize::MIN && y < usize::MAX && self.get(x - 1, y + 1), ]; if neighbors.iter().filter(|i| **i).count() < neighbors.len() { - edge_points.push(Vec2::new(x as f32, y as f32)); + edge_points.push((x, y)); } } @@ -108,42 +100,42 @@ impl Edges { /// /// 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. - fn points_to_drawing_order(&self, points: Vec, translate: bool) -> Vec> { - let mut groups: Vec> = Vec::new(); + fn points_to_drawing_order(&self, points: Vec, translate: bool) -> Vec> { + let mut groups: Vec> = Vec::new(); if points.is_empty() { - return groups; + return Vec::new(); } - let mut in_drawing_order: Vec = Vec::new(); - let mut drawn_points_with_counts: MashMap<(OrderedFloat, OrderedFloat), ()> = - MashMap::new(); - let mut drawn_points: HashSet<(OrderedFloat, OrderedFloat)> = HashSet::new(); - let hashable = |v: Vec2| (OrderedFloat(v.x), OrderedFloat(v.y)); + let mut in_drawing_order: Vec = Vec::new(); + let mut drawn_points_with_counts: MashMap = MashMap::new(); + let mut drawn_points: HashSet = HashSet::new(); + // d=√((x2-x1)²+(y2-y1)²) - let distance = |a: Vec2, b: Vec2| -> f32 { - ((a.x - b.x).abs().powi(2) + (a.y - b.y).abs().powi(2)).sqrt() + let distance = |a: Point, b: Point| -> f32 { + ((a.0 as f32 - b.0 as f32).abs().powi(2) + (a.1 as f32 - b.1 as f32).abs().powi(2)) + .sqrt() }; let mut start = points[0]; let mut current = start; in_drawing_order.push(current); - drawn_points_with_counts.insert(hashable(current), ()); - drawn_points.insert(hashable(current)); + drawn_points_with_counts.insert(current, ()); + drawn_points.insert(current); while drawn_points.len() < points.len() { - let neighbors = &points + let neighbors: Vec<&Point> = points .iter() .filter(|p| (distance(current, **p) - 1.0).abs() < 0.000_000_1) - .collect::>(); + .collect(); if let Some(p) = neighbors .iter() - .min_by_key(|n| drawn_points_with_counts.get_iter(&hashable(***n)).count()) + .min_by_key(|n| drawn_points_with_counts.get_iter(**n).count()) { current = **p; in_drawing_order.push(**p); - drawn_points_with_counts.insert(hashable(**p), ()); - drawn_points.insert(hashable(**p)); + drawn_points_with_counts.insert(**p, ()); + drawn_points.insert(**p); } // we've traversed and backtracked and we're back at the start without reaching the end of the points @@ -155,13 +147,10 @@ impl Edges { in_drawing_order.clear(); drawn_points_with_counts.clear(); - if let Some(c) = points - .iter() - .find(|p| !drawn_points.contains(&hashable(**p))) - { + if let Some(c) = points.iter().find(|p| !drawn_points.contains(&**p)) { in_drawing_order.push(*c); - drawn_points_with_counts.insert(hashable(*c), ()); - drawn_points.insert(hashable(*c)); + drawn_points_with_counts.insert(*c, ()); + drawn_points.insert(*c); current = *c; start = current; } else { @@ -172,11 +161,15 @@ impl Edges { groups.push(in_drawing_order.clone()); + let groups = groups + .into_iter() + .map(|v| v.into_iter().map(|p| Vec2::new(p.0 as f32, p.1 as f32))); + if translate { - groups = groups.into_iter().map(|p| self.translate_vec(p)).collect(); + groups.map(|p| self.translate(p)).collect() + } else { + groups.map(Iterator::collect).collect() } - - groups } /// conceptual helper, access a 1D vector like it's a 2D vector @@ -188,9 +181,9 @@ impl Edges { /// get zero or non-zero pixel the value at given coordinate fn get(&self, x: usize, y: usize) -> bool { - let index = y * self.width / 8 + x / 8; - if let Some(mut byte) = self.data.get(index).copied() { - byte >>= 7 - x % 8; + let index = y * self.width + x; + if let Some(mut byte) = self.data.get(index / 8).copied() { + byte >>= index % 8; byte & 1 > 0 } else { false @@ -207,8 +200,11 @@ impl Edges { /// Translate vector of points in positive x,y to either side of (0,0) #[must_use] - pub fn translate_vec(&self, v: Vec) -> Vec { - v.into_iter().map(|p| self.translate_point(p)).collect() + pub fn translate(&self, v: T) -> Vec + where + T: Iterator, + { + v.map(|p| self.translate_point(p)).collect() } } From 334361c7c1acca3e3e548b679046c5117f087de2 Mon Sep 17 00:00:00 2001 From: salam Date: Wed, 23 Oct 2024 20:53:34 +0900 Subject: [PATCH 11/32] rework of Edges Added: - function `new` to take `Edges` from any data. - `BinImage` structure to represent an image. - `Point` alias to represent a point in the image. Moved: - structure `Edges` moved to lib.rs. - tests to separate file. - `distance` function to utils. Removed: - `hashbrown`, `mashmap` and `thiserror` from dependencies. --- Cargo.toml | 3 - src/bin_image.rs | 69 +++++++++++ src/edges.rs | 312 ----------------------------------------------- src/lib.rs | 181 ++++++++++++++++++++++++++- src/tests.rs | 58 +++++++++ src/utils.rs | 6 + 6 files changed, 312 insertions(+), 317 deletions(-) create mode 100644 src/bin_image.rs delete mode 100644 src/edges.rs create mode 100644 src/tests.rs create mode 100644 src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 62b1e3a..7619b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,7 @@ glam-latest = ["dep:glam"] bevy = ["dep:bevy_math", "dep:bevy_render"] [dependencies] -hashbrown = "0.15" image = "0.25" -mashmap = "0.2" -thiserror = "1.0" [dependencies.glam] version = "0.29" diff --git a/src/bin_image.rs b/src/bin_image.rs new file mode 100644 index 0000000..4645c36 --- /dev/null +++ b/src/bin_image.rs @@ -0,0 +1,69 @@ +use crate::{utils::Point, Vec2}; + +pub struct BinImage { + data: Vec, + pub height: usize, + pub width: usize, +} + +impl BinImage { + pub fn new(height: usize, width: usize, data: &[u8]) -> Self { + let compress_step = data.len() / (height * width); + Self { + data: data + .chunks(8 * compress_step) + .map(|chunk| { + chunk + .chunks(compress_step) + .map(|chunk| chunk.iter().any(|i| *i != 0)) + .enumerate() + .map(|(index, bit)| u8::from(bit) << index) + .sum() + }) + .collect(), + height, + width, + } + } + + /// get pixel value at given coordinate + pub fn get(&self, (x, y): Point) -> bool { + let index = y * self.width + x; + if let Some(mut byte) = self.data.get(index / 8 /* index of byte */).copied() { + byte >>= index % 8; // index of bit + x <= self.width && byte & 1 > 0 + } else { + false + } + } + + pub fn get_neighbors(&self, (x, y): Point) -> [bool; 8] { + [ + y < usize::MAX && self.get((x, y + 1)), + y > usize::MIN && self.get((x, y - 1)), + x < usize::MAX && self.get((x + 1, y)), + x > usize::MIN && self.get((x - 1, y)), + x < usize::MAX && y < usize::MAX && self.get((x + 1, y + 1)), + x > usize::MIN && y > usize::MIN && self.get((x - 1, y - 1)), + x < usize::MAX && y > usize::MIN && self.get((x + 1, y - 1)), + x > usize::MIN && y < usize::MAX && self.get((x - 1, y + 1)), + ] + } + + /// translate point in positive x,y to either side of (0,0) + 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, + ) + } + + /// Translate iterator of points in positive x,y to either side of (0,0) + #[must_use] + pub fn translate(&self, v: T) -> Vec + where + T: Iterator, + { + v.map(|p| self.translate_point(p)).collect() + } +} diff --git a/src/edges.rs b/src/edges.rs deleted file mode 100644 index 08ca326..0000000 --- a/src/edges.rs +++ /dev/null @@ -1,312 +0,0 @@ -#[cfg(feature = "bevy")] -pub use bevy_math::prelude::Vec2; -#[cfg(not(feature = "bevy"))] -pub use glam::Vec2; -use std::fmt; - -use hashbrown::HashSet; -use mashmap::MashMap; - -type Point = (usize, usize); - -pub struct Edges { - data: Vec, - height: usize, - width: usize, -} - -impl Edges { - #[must_use] - pub fn new(height: usize, width: usize, data: &[u8]) -> Self { - Self { - height, - width, - data: { - let compress_step = data.len() / (height * width); - data.chunks(compress_step) - .map(|chunk| chunk.iter().any(|i| *i != 0)) - .collect::>() - .chunks(8) - .map(|byte| { - byte.iter() - .enumerate() - .map(|(i, bit)| u8::from(*bit) << i) - .sum::() - }) - .collect() - }, - } - } - /// If there's only one sprite / object in the image, this returns just one, with - /// 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() - } - - /// 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() - } - - /// 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) - #[must_use] - pub fn multi_image_edge_translated(&self) -> Vec> { - self.image_edges(true) - } - - /// 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 - #[must_use] - pub fn multi_image_edges_raw(&self) -> Vec> { - self.image_edges(false) - } - - /// 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> { - let mut edge_points = Vec::new(); - // 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 - for i in 0..self.width * self.height { - let (x, y) = self.get_pos(i); - if !self.get(x, y) { - continue; - } - let neighbors = [ - y < usize::MAX && self.get(x, y + 1), - y > usize::MIN && self.get(x, y - 1), - x < usize::MAX && self.get(x + 1, y), - x > usize::MIN && self.get(x - 1, y), - x < usize::MAX && y < usize::MAX && self.get(x + 1, y + 1), - x > usize::MIN && y > usize::MIN && self.get(x - 1, y - 1), - x < usize::MAX && y > usize::MIN && self.get(x + 1, y - 1), - x > usize::MIN && y < usize::MAX && self.get(x - 1, y + 1), - ]; - if neighbors.iter().filter(|i| **i).count() < neighbors.len() { - edge_points.push((x, y)); - } - } - - self.points_to_drawing_order(edge_points, translate) - } - - /// 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. - fn points_to_drawing_order(&self, points: Vec, translate: bool) -> Vec> { - let mut groups: Vec> = Vec::new(); - if points.is_empty() { - return Vec::new(); - } - - let mut in_drawing_order: Vec = Vec::new(); - let mut drawn_points_with_counts: MashMap = MashMap::new(); - let mut drawn_points: HashSet = HashSet::new(); - - // d=√((x2-x1)²+(y2-y1)²) - let distance = |a: Point, b: Point| -> f32 { - ((a.0 as f32 - b.0 as f32).abs().powi(2) + (a.1 as f32 - b.1 as f32).abs().powi(2)) - .sqrt() - }; - - let mut start = points[0]; - let mut current = start; - in_drawing_order.push(current); - drawn_points_with_counts.insert(current, ()); - drawn_points.insert(current); - - while drawn_points.len() < points.len() { - let neighbors: Vec<&Point> = points - .iter() - .filter(|p| (distance(current, **p) - 1.0).abs() < 0.000_000_1) - .collect(); - - if let Some(p) = neighbors - .iter() - .min_by_key(|n| drawn_points_with_counts.get_iter(**n).count()) - { - current = **p; - in_drawing_order.push(**p); - drawn_points_with_counts.insert(**p, ()); - drawn_points.insert(**p); - } - - // 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 - _ = in_drawing_order.pop(); - groups.push(in_drawing_order.clone()); - in_drawing_order.clear(); - drawn_points_with_counts.clear(); - - if let Some(c) = points.iter().find(|p| !drawn_points.contains(&**p)) { - in_drawing_order.push(*c); - drawn_points_with_counts.insert(*c, ()); - drawn_points.insert(*c); - current = *c; - start = current; - } else { - break; - } - } - } - - groups.push(in_drawing_order.clone()); - - let groups = groups - .into_iter() - .map(|v| v.into_iter().map(|p| Vec2::new(p.0 as f32, p.1 as f32))); - - if translate { - groups.map(|p| self.translate(p)).collect() - } else { - groups.map(Iterator::collect).collect() - } - } - - /// conceptual helper, access a 1D vector like it's a 2D vector - fn get_pos(&self, index: usize) -> (usize, usize) { - let quot = index / self.height; - let rem = index % self.height; - (quot, rem) - } - - /// get zero or non-zero pixel the value at given coordinate - fn get(&self, x: usize, y: usize) -> bool { - let index = y * self.width + x; - if let Some(mut byte) = self.data.get(index / 8).copied() { - byte >>= index % 8; - byte & 1 > 0 - } else { - false - } - } - - /// translate point in positive x,y to either side of (0,0) - 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, - ) - } - - /// Translate vector of points in positive x,y to either side of (0,0) - #[must_use] - pub fn translate(&self, v: T) -> Vec - where - T: Iterator, - { - v.map(|p| self.translate_point(p)).collect() - } -} - -#[cfg(feature = "bevy")] -impl From for Edges { - fn from(i: bevy_render::prelude::Image) -> Edges { - Self::new(i.height() as usize, i.width() as usize, &i.data) - } -} - -impl From for Edges { - fn from(i: image::DynamicImage) -> Edges { - Self::new(i.height() as usize, i.width() as usize, i.as_bytes()) - } -} - -impl From<&T> for Edges -where - T: Clone, - Edges: From, -{ - fn from(value: &T) -> Self { - Self::from(value.clone()) - } -} - -impl fmt::Debug for Edges { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - #[derive(Debug)] - #[allow(dead_code)] - struct EdgesDisplay { - raw: Vec>, - translated: Vec>, - } - - let edges_display = EdgesDisplay { - raw: self.image_edges(false), - translated: self.image_edges(true), - }; - write!(f, "{edges_display:#?}") - } -} - -#[cfg(feature = "bevy")] -#[cfg(test)] -mod tests { - use crate::Edges; - use bevy_render::{ - render_asset::RenderAssetUsages, - texture::{CompressedImageFormats, Image, ImageSampler, ImageType}, - }; - use std::path::Path; - - #[test] - fn same_image_same_edges() { - let dynamic_image = image::open(Path::new("assets/car.png")).unwrap(); - let dynamic_edges = Edges::from(dynamic_image); - - let bevy_image = Image::from_buffer( - include_bytes!("../assets/car.png"), // buffer - ImageType::Extension("png"), - CompressedImageFormats::default(), - true, // - ImageSampler::default(), - RenderAssetUsages::default(), - ) - .unwrap(); - let bevy_edges = Edges::from(bevy_image); - - assert_eq!( - dynamic_edges.single_image_edge_raw(), - bevy_edges.single_image_edge_raw() - ); - assert_eq!( - dynamic_edges.single_image_edge_translated(), - bevy_edges.single_image_edge_translated() - ); - } - - #[test] - fn same_images_same_edges() { - let dynamic_image = image::open(Path::new("assets/boulders.png")).unwrap(); - let dynamic_edges = Edges::from(dynamic_image); - - let bevy_image = Image::from_buffer( - include_bytes!("../assets/boulders.png"), // buffer - ImageType::Extension("png"), - CompressedImageFormats::default(), - true, // - ImageSampler::default(), - RenderAssetUsages::default(), - ) - .unwrap(); - let bevy_edges = Edges::from(bevy_image); - - assert_eq!( - dynamic_edges.multi_image_edges_raw(), - bevy_edges.multi_image_edges_raw() - ); - assert_eq!( - dynamic_edges.multi_image_edge_translated(), - bevy_edges.multi_image_edge_translated() - ); - } -} diff --git a/src/lib.rs b/src/lib.rs index 26fd67b..45cb803 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,182 @@ #![doc = include_str!("../README.md")] -mod edges; +#[cfg(feature = "bevy")] +pub use bevy_math::prelude::Vec2; +#[cfg(not(feature = "bevy"))] +pub use glam::Vec2; +use std::{collections::HashMap, fmt}; -pub use edges::*; +use crate::{ + bin_image::BinImage, + utils::{distance, Point}, +}; + +mod bin_image; +#[cfg(feature = "bevy")] +#[cfg(test)] +mod tests; +mod utils; + +pub struct Edges { + image: BinImage, +} + +impl Edges { + #[must_use] + pub fn new(height: usize, width: usize, data: &[u8]) -> Self { + Self { + image: BinImage::new(height, width, data), + } + } + /// If there's only one sprite / object in the image, this returns just one, with + /// 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() + } + + /// 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() + } + + /// 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) + #[must_use] + pub fn multi_image_edge_translated(&self) -> Vec> { + self.image_edges(true) + } + + /// 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 + #[must_use] + pub fn multi_image_edges_raw(&self) -> Vec> { + self.image_edges(false) + } + + /// 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> { + 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 + // order, groups them into sets of connected pixels + let edge_points = (0..image.height * image.width) + .map(|i| (i / image.height, i % image.height)) + .filter(|p| image.get(*p)) + .filter(|p| (0..8).contains(&image.get_neighbors(*p).iter().filter(|i| **i).count())) + .collect(); + + self.points_to_drawing_order(edge_points, translate) + } + + /// 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. + fn points_to_drawing_order(&self, points: Vec, translate: bool) -> Vec> { + if points.is_empty() { + return Vec::new(); + } + + let mut groups: Vec> = Vec::new(); + let mut group: Vec = Vec::new(); + let mut drawn_points_with_counts = HashMap::new(); + + let mut start = points[0]; + let mut current = start; + group.push(current); + drawn_points_with_counts.insert(current, 2); + + while drawn_points_with_counts.len() < points.len() { + if let Some(p) = points + .iter() + .filter(|p| (distance(current, p) - 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); + } + } + + // 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; + } + + 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; + } + } + } + groups.push(group); + + let groups = groups + .into_iter() + .map(|v| v.into_iter().map(|(x, y)| Vec2::new(x as f32, y as f32))); + + if translate { + groups.map(|p| self.image.translate(p)).collect() + } else { + groups.map(Iterator::collect).collect() + } + } +} + +#[cfg(feature = "bevy")] +impl From for Edges { + fn from(i: bevy_render::prelude::Image) -> Edges { + Self::new(i.height() as usize, i.width() as usize, &i.data) + } +} + +impl From for Edges { + fn from(i: image::DynamicImage) -> Edges { + Self::new(i.height() as usize, i.width() as usize, i.as_bytes()) + } +} + +impl From<&T> for Edges +where + T: Clone, + Edges: From, +{ + fn from(value: &T) -> Self { + Self::from(value.clone()) + } +} + +impl fmt::Debug for Edges { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "EdgesDisplay {{ + raw: {:#?}, + translated: {:#?} +}}", + self.image_edges(false), + self.image_edges(true), + ) + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..4611555 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,58 @@ +use crate::Edges; +use bevy_render::{ + render_asset::RenderAssetUsages, + texture::{CompressedImageFormats, Image, ImageSampler, ImageType}, +}; +use std::path::Path; + +#[test] +fn same_image_same_edges() { + let dynamic_image = image::open(Path::new("assets/car.png")).unwrap(); + let dynamic_edges = Edges::from(dynamic_image); + + let bevy_image = Image::from_buffer( + include_bytes!("../assets/car.png"), // buffer + ImageType::Extension("png"), + CompressedImageFormats::default(), + true, // + ImageSampler::default(), + RenderAssetUsages::default(), + ) + .unwrap(); + let bevy_edges = Edges::from(bevy_image); + + assert_eq!( + dynamic_edges.single_image_edge_raw(), + bevy_edges.single_image_edge_raw() + ); + assert_eq!( + dynamic_edges.single_image_edge_translated(), + bevy_edges.single_image_edge_translated() + ); +} + +#[test] +fn same_images_same_edges() { + let dynamic_image = image::open(Path::new("assets/boulders.png")).unwrap(); + let dynamic_edges = Edges::from(dynamic_image); + + let bevy_image = Image::from_buffer( + include_bytes!("../assets/boulders.png"), // buffer + ImageType::Extension("png"), + CompressedImageFormats::default(), + true, // + ImageSampler::default(), + RenderAssetUsages::default(), + ) + .unwrap(); + let bevy_edges = Edges::from(bevy_image); + + assert_eq!( + dynamic_edges.multi_image_edges_raw(), + bevy_edges.multi_image_edges_raw() + ); + assert_eq!( + dynamic_edges.multi_image_edge_translated(), + bevy_edges.multi_image_edge_translated() + ); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..511383e --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,6 @@ +pub type Point = (usize, usize); + +// d=√((x2-x1)²+(y2-y1)²) +pub fn distance((x2, y2): Point, (x1, y1): &Point) -> f32 { + ((x2 as f32 - *x1 as f32).powi(2) + (y2 as f32 - *y1 as f32).powi(2)).sqrt() +} From 7fcb96260cf8aa833108332657eae7d49a222e73 Mon Sep 17 00:00:00 2001 From: salam Date: Thu, 24 Oct 2024 00:16:34 +0900 Subject: [PATCH 12/32] changed type of size params from usize to u32 --- src/bin_image.rs | 30 +++++++++++++++++------------- src/lib.rs | 26 +++++++++++++++----------- src/utils.rs | 6 +++--- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/bin_image.rs b/src/bin_image.rs index 4645c36..fdeae85 100644 --- a/src/bin_image.rs +++ b/src/bin_image.rs @@ -2,13 +2,13 @@ use crate::{utils::Point, Vec2}; pub struct BinImage { data: Vec, - pub height: usize, - pub width: usize, + pub height: u32, + pub width: u32, } impl BinImage { - pub fn new(height: usize, width: usize, data: &[u8]) -> Self { - let compress_step = data.len() / (height * width); + pub fn new(height: u32, width: u32, data: &[u8]) -> Self { + let compress_step = data.len() / (height * width) as usize; Self { data: data .chunks(8 * compress_step) @@ -29,7 +29,11 @@ impl BinImage { /// get pixel value at given coordinate pub fn get(&self, (x, y): Point) -> bool { let index = y * self.width + x; - if let Some(mut byte) = self.data.get(index / 8 /* index of byte */).copied() { + 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 } else { @@ -39,14 +43,14 @@ impl BinImage { pub fn get_neighbors(&self, (x, y): Point) -> [bool; 8] { [ - y < usize::MAX && self.get((x, y + 1)), - y > usize::MIN && self.get((x, y - 1)), - x < usize::MAX && self.get((x + 1, y)), - x > usize::MIN && self.get((x - 1, y)), - x < usize::MAX && y < usize::MAX && self.get((x + 1, y + 1)), - x > usize::MIN && y > usize::MIN && self.get((x - 1, y - 1)), - x < usize::MAX && y > usize::MIN && self.get((x + 1, y - 1)), - x > usize::MIN && y < usize::MAX && self.get((x - 1, y + 1)), + y < u32::MAX && self.get((x, y + 1)), + y > u32::MIN && self.get((x, y - 1)), + x < u32::MAX && self.get((x + 1, y)), + x > u32::MIN && self.get((x - 1, y)), + x < u32::MAX && y < u32::MAX && self.get((x + 1, y + 1)), + x > u32::MIN && y > u32::MIN && self.get((x - 1, y - 1)), + x < u32::MAX && y > u32::MIN && self.get((x + 1, y - 1)), + x > u32::MIN && y < u32::MAX && self.get((x - 1, y + 1)), ] } diff --git a/src/lib.rs b/src/lib.rs index 45cb803..3d1b378 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,11 +23,12 @@ pub struct Edges { impl Edges { #[must_use] - pub fn new(height: usize, width: usize, data: &[u8]) -> Self { + pub fn new(height: u32, width: u32, data: &[u8]) -> Self { Self { image: BinImage::new(height, width, data), } } + /// If there's only one sprite / object in the image, this returns just one, with /// coordinates translated to either side of (0, 0) #[must_use] @@ -94,7 +95,7 @@ impl Edges { while drawn_points_with_counts.len() < points.len() { if let Some(p) = points .iter() - .filter(|p| (distance(current, p) - 1.0).abs() <= f32::EPSILON) + .filter(|p| (distance(current, **p) - 1.0).abs() <= f32::EPSILON) .min_by_key(|n| drawn_points_with_counts.get(n).map_or(0, |c| *c)) { current = *p; @@ -147,23 +148,26 @@ impl Edges { #[cfg(feature = "bevy")] impl From for Edges { fn from(i: bevy_render::prelude::Image) -> Edges { - Self::new(i.height() as usize, i.width() as usize, &i.data) + Self::new(i.height(), i.width(), &i.data) } } impl From for Edges { fn from(i: image::DynamicImage) -> Edges { - Self::new(i.height() as usize, i.width() as usize, i.as_bytes()) + Self::new(i.height(), i.width(), i.as_bytes()) + } +} + +#[cfg(feature = "bevy")] +impl From<&bevy_render::prelude::Image> for Edges { + fn from(i: &bevy_render::prelude::Image) -> Edges { + Self::new(i.height(), i.width(), &i.data) } } -impl From<&T> for Edges -where - T: Clone, - Edges: From, -{ - fn from(value: &T) -> Self { - Self::from(value.clone()) +impl From<&image::DynamicImage> for Edges { + fn from(i: &image::DynamicImage) -> Edges { + Self::new(i.height(), i.width(), i.as_bytes()) } } diff --git a/src/utils.rs b/src/utils.rs index 511383e..a707469 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ -pub type Point = (usize, usize); +pub type Point = (u32, u32); // d=√((x2-x1)²+(y2-y1)²) -pub fn distance((x2, y2): Point, (x1, y1): &Point) -> f32 { - ((x2 as f32 - *x1 as f32).powi(2) + (y2 as f32 - *y1 as f32).powi(2)).sqrt() +pub fn distance((x2, y2): Point, (x1, y1): Point) -> f32 { + ((x2 as f32 - x1 as f32).powi(2) + (y2 as f32 - y1 as f32).powi(2)).sqrt() } From 805387d068cd7a10dafc85f9b2a618cb1223054a Mon Sep 17 00:00:00 2001 From: salam Date: Thu, 24 Oct 2024 15:31:32 +0900 Subject: [PATCH 13/32] the representation of points is now `UVec2` --- src/bin_image.rs | 33 ++++++++++++++++++--------------- src/lib.rs | 20 +++++++++----------- src/utils.rs | 6 +++--- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/bin_image.rs b/src/bin_image.rs index fdeae85..0c9de34 100644 --- a/src/bin_image.rs +++ b/src/bin_image.rs @@ -1,4 +1,4 @@ -use crate::{utils::Point, Vec2}; +use crate::{UVec2, Vec2}; pub struct BinImage { data: Vec, @@ -8,6 +8,10 @@ pub struct BinImage { impl BinImage { pub fn new(height: u32, width: u32, data: &[u8]) -> Self { + assert!( + data.len() >= (height * width) as usize, + "data must not be smaller than image dimensions" + ); let compress_step = data.len() / (height * width) as usize; Self { data: data @@ -26,12 +30,12 @@ impl BinImage { } } - /// get pixel value at given coordinate - pub fn get(&self, (x, y): Point) -> bool { + pub fn get(&self, p: UVec2) -> bool { + let (x, y) = (p.x, p.y); let index = y * self.width + x; if let Some(mut byte) = self .data - .get((index / 8) as usize /* index of byte */) + .get((index / 8) as usize) // index of byte .copied() { byte >>= index % 8; // index of bit @@ -41,16 +45,17 @@ impl BinImage { } } - pub fn get_neighbors(&self, (x, y): Point) -> [bool; 8] { + pub fn get_neighbors(&self, p: UVec2) -> [bool; 8] { + let (x, y) = (p.x, p.y); [ - y < u32::MAX && self.get((x, y + 1)), - y > u32::MIN && self.get((x, y - 1)), - x < u32::MAX && self.get((x + 1, y)), - x > u32::MIN && self.get((x - 1, y)), - x < u32::MAX && y < u32::MAX && self.get((x + 1, y + 1)), - x > u32::MIN && y > u32::MIN && self.get((x - 1, y - 1)), - x < u32::MAX && y > u32::MIN && self.get((x + 1, y - 1)), - x > u32::MIN && y < u32::MAX && self.get((x - 1, y + 1)), + 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 ] } @@ -62,8 +67,6 @@ impl BinImage { ) } - /// Translate iterator of points in positive x,y to either side of (0,0) - #[must_use] pub fn translate(&self, v: T) -> Vec where T: Iterator, diff --git a/src/lib.rs b/src/lib.rs index 3d1b378..75db66a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,12 @@ #![doc = include_str!("../README.md")] #[cfg(feature = "bevy")] -pub use bevy_math::prelude::Vec2; +pub use bevy_math::prelude::{UVec2, Vec2}; #[cfg(not(feature = "bevy"))] -pub use glam::Vec2; +pub use glam::{UVec2, Vec2}; use std::{collections::HashMap, fmt}; -use crate::{ - bin_image::BinImage, - utils::{distance, Point}, -}; +use crate::{bin_image::BinImage, utils::distance}; mod bin_image; #[cfg(feature = "bevy")] @@ -67,6 +64,7 @@ impl Edges { // 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())) .collect(); @@ -78,13 +76,13 @@ impl Edges { /// /// 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. - fn points_to_drawing_order(&self, points: Vec, translate: bool) -> Vec> { + fn points_to_drawing_order(&self, points: Vec, translate: bool) -> Vec> { if points.is_empty() { return Vec::new(); } - let mut groups: Vec> = Vec::new(); - let mut group: Vec = Vec::new(); + let mut groups: Vec> = Vec::new(); + let mut group: Vec = Vec::new(); let mut drawn_points_with_counts = HashMap::new(); let mut start = points[0]; @@ -95,7 +93,7 @@ impl Edges { while drawn_points_with_counts.len() < points.len() { if let Some(p) = points .iter() - .filter(|p| (distance(current, **p) - 1.0).abs() <= f32::EPSILON) + .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; @@ -135,7 +133,7 @@ impl Edges { let groups = groups .into_iter() - .map(|v| v.into_iter().map(|(x, y)| Vec2::new(x as f32, y as f32))); + .map(|v| v.into_iter().map(|p| p.as_vec2())); if translate { groups.map(|p| self.image.translate(p)).collect() diff --git a/src/utils.rs b/src/utils.rs index a707469..00a99f5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ -pub type Point = (u32, u32); +use crate::Vec2; // d=√((x2-x1)²+(y2-y1)²) -pub fn distance((x2, y2): Point, (x1, y1): Point) -> f32 { - ((x2 as f32 - x1 as f32).powi(2) + (y2 as f32 - y1 as f32).powi(2)).sqrt() +pub fn distance(a: Vec2, b: Vec2) -> f32 { + ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt() } From f810b7cea821ca4a94ab9d1d3f89fcc07417defc Mon Sep 17 00:00:00 2001 From: salam Date: Thu, 24 Oct 2024 15:34:13 +0900 Subject: [PATCH 14/32] added documentation for `BinImage` methods --- src/bin_image.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/bin_image.rs b/src/bin_image.rs index 0c9de34..6524eda 100644 --- a/src/bin_image.rs +++ b/src/bin_image.rs @@ -7,6 +7,25 @@ pub struct BinImage { } impl BinImage { + /// Creates a new `BinImage` from the given height, width, and raw pixel data. + /// + /// # Arguments + /// + /// * `height` - The height of the image in pixels. + /// * `width` - The width of the image in pixels. + /// * `data` - A slice of bytes representing the raw pixel data. The length of this slice + /// must be at least `height * width`. + /// + /// # Panics + /// + /// This function will panic if the length of `data` is less than `height * width`. + /// + /// # Example + /// + /// ``` + /// let raw_data = vec![0, 1, 0, 1, 0, 0, 1, 1]; // Example raw data + /// let image = BinImage::new(2, 4, &raw_data); + /// ``` pub fn new(height: u32, width: u32, data: &[u8]) -> Self { assert!( data.len() >= (height * width) as usize, @@ -30,6 +49,15 @@ impl BinImage { } } + /// Gets the pixel value at the given coordinate. + /// + /// # Arguments + /// + /// * `p` - A `UVec2` representing the coordinates of the pixel. + /// + /// # Returns + /// + /// 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; @@ -45,6 +73,15 @@ impl BinImage { } } + /// 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 array of 8 boolean values representing the state of the neighboring pixels. pub fn get_neighbors(&self, p: UVec2) -> [bool; 8] { let (x, y) = (p.x, p.y); [ @@ -59,7 +96,15 @@ impl BinImage { ] } - /// translate point in positive x,y to either side of (0,0) + /// 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 { Vec2::new( p.x - (self.width as f32 / 2.0 - 1.0), @@ -67,6 +112,15 @@ impl BinImage { ) } + /// Translates an iterator of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). + /// + /// # Arguments + /// + /// * `v` - An iterator of `Vec2` points to translate. + /// + /// # Returns + /// + /// A vector of `Vec2` representing the translated coordinates. pub fn translate(&self, v: T) -> Vec where T: Iterator, From 7556dee606fe5d2e01f232813832750df02e766f Mon Sep 17 00:00:00 2001 From: salam Date: Sat, 26 Oct 2024 00:10:14 +0900 Subject: [PATCH 15/32] removed example for `BinImage` The example fails doc tests because `BinImage` is a private structure. --- src/bin_image.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/bin_image.rs b/src/bin_image.rs index 6524eda..9b2e5e1 100644 --- a/src/bin_image.rs +++ b/src/bin_image.rs @@ -19,13 +19,6 @@ impl BinImage { /// # Panics /// /// This function will panic if the length of `data` is less than `height * width`. - /// - /// # Example - /// - /// ``` - /// let raw_data = vec![0, 1, 0, 1, 0, 0, 1, 1]; // Example raw data - /// let image = BinImage::new(2, 4, &raw_data); - /// ``` pub fn new(height: u32, width: u32, data: &[u8]) -> Self { assert!( data.len() >= (height * width) as usize, From 55737517a246b207e87c8abf99d6fbe3d3786e0a Mon Sep 17 00:00:00 2001 From: salam Date: Sat, 26 Oct 2024 13:25:25 +0900 Subject: [PATCH 16/32] function `translate` returned to `Edges` `width` and `height` fields are now private to ensure `BingImage` is immutable. To get `width` and `height` there are methods of the same name in `BinImage`. The `points_to_drawing_order` function has been moved out of the `Edges` implementation, as the usage of the `translate` argument has been moved to `image_edges`. --- src/bin_image.rs | 19 +++++--- src/lib.rs | 111 ++++++++++++++--------------------------------- src/utils.rs | 65 ++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 86 deletions(-) diff --git a/src/bin_image.rs b/src/bin_image.rs index 9b2e5e1..a402c4f 100644 --- a/src/bin_image.rs +++ b/src/bin_image.rs @@ -2,8 +2,8 @@ use crate::{UVec2, Vec2}; pub struct BinImage { data: Vec, - pub height: u32, - pub width: u32, + height: u32, + width: u32, } impl BinImage { @@ -114,10 +114,15 @@ impl BinImage { /// # Returns /// /// A vector of `Vec2` representing the translated coordinates. - pub fn translate(&self, v: T) -> Vec - where - T: Iterator, - { - v.map(|p| self.translate_point(p)).collect() + pub fn translate(&self, v: Vec) -> Vec { + v.into_iter().map(|p| self.translate_point(p)).collect() + } + + pub const fn height(&self) -> u32 { + self.height + } + + pub const fn width(&self) -> u32 { + self.width } } diff --git a/src/lib.rs b/src/lib.rs index 75db66a..459b90a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,9 @@ pub use bevy_math::prelude::{UVec2, Vec2}; #[cfg(not(feature = "bevy"))] pub use glam::{UVec2, Vec2}; -use std::{collections::HashMap, fmt}; +use std::fmt; -use crate::{bin_image::BinImage, utils::distance}; +use crate::{bin_image::BinImage, utils::points_to_drawing_order}; mod bin_image; #[cfg(feature = "bevy")] @@ -62,84 +62,38 @@ 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)) + 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())) .collect(); - self.points_to_drawing_order(edge_points, translate) - } - - /// 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. - fn points_to_drawing_order(&self, points: Vec, translate: bool) -> Vec> { - if points.is_empty() { - return Vec::new(); - } - - let mut groups: Vec> = Vec::new(); - let mut group: Vec = Vec::new(); - let mut drawn_points_with_counts = HashMap::new(); - - let mut start = points[0]; - let mut current = start; - group.push(current); - drawn_points_with_counts.insert(current, 2); - - 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); - } - } - - // 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; - } - - 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); + 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 { - break; + group } - } - } - groups.push(group); - - let groups = groups - .into_iter() - .map(|v| v.into_iter().map(|p| p.as_vec2())); + }) + .collect() + } - if translate { - groups.map(|p| self.image.translate(p)).collect() - } else { - groups.map(Iterator::collect).collect() - } + /// Translates an iterator of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). + /// + /// # Arguments + /// + /// * `v` - An iterator of `Vec2` points to translate. + /// + /// # Returns + /// + /// A vector of `Vec2` representing the translated coordinates. + #[must_use] + pub fn translate(&self, v: Vec) -> Vec { + self.image.translate(v) } } @@ -173,12 +127,13 @@ impl fmt::Debug for Edges { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "EdgesDisplay {{ - raw: {:#?}, - translated: {:#?} -}}", - self.image_edges(false), - self.image_edges(true), + "{}", + format!( + "Edges {{\nraw: {:#?},\ntranslated: {:#?}\n}}", + self.image_edges(false), + self.image_edges(true), + ) + .replace('\n', "\n "), ) } } diff --git a/src/utils.rs b/src/utils.rs index 00a99f5..07cd18d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,69 @@ -use crate::Vec2; +use crate::{UVec2, Vec2}; +use std::collections::HashMap; // 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() } + +/// 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(); + } + + let mut groups: Vec> = Vec::new(); + let mut group: Vec = Vec::new(); + let mut drawn_points_with_counts = HashMap::new(); + + let mut start = points[0]; + let mut current = start; + group.push(current); + drawn_points_with_counts.insert(current, 2); + + 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); + } + } + + // 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; + } + + 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; + } + } + } + groups.push(group); + + groups +} From 5301356b8317a4f2c2cf702e090345c4ba5c4122 Mon Sep 17 00:00:00 2001 From: salam Date: Sat, 26 Oct 2024 17:56:23 +0900 Subject: [PATCH 17/32] fix(doc): function `translate` --- src/bin_image.rs | 4 ++-- src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bin_image.rs b/src/bin_image.rs index a402c4f..896a7e2 100644 --- a/src/bin_image.rs +++ b/src/bin_image.rs @@ -105,11 +105,11 @@ impl BinImage { ) } - /// Translates an iterator of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). + /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). /// /// # Arguments /// - /// * `v` - An iterator of `Vec2` points to translate. + /// * `v` - An `Vec` of `Vec2` points to translate. /// /// # Returns /// diff --git a/src/lib.rs b/src/lib.rs index 459b90a..ec37732 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,11 +82,11 @@ impl Edges { .collect() } - /// Translates an iterator of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). + /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). /// /// # Arguments /// - /// * `v` - An iterator of `Vec2` points to translate. + /// * `v` - An `Vec` of `Vec2` points to translate. /// /// # Returns /// From 60cb046930b899926877e62dd5700dfc37ec32b8 Mon Sep 17 00:00:00 2001 From: salam Date: Wed, 30 Oct 2024 18:14:57 +0900 Subject: [PATCH 18/32] reworking edges search algoritm --- Cargo.toml | 2 - src/{bin_image.rs => bin_image/mod.rs} | 45 +++-- src/bin_image/neighbors.rs | 8 + src/lib.rs | 70 +++++-- src/utils.rs | 250 +++++++++++++++++++------ 5 files changed, 284 insertions(+), 91 deletions(-) rename src/{bin_image.rs => bin_image/mod.rs} (71%) create mode 100644 src/bin_image/neighbors.rs diff --git a/Cargo.toml b/Cargo.toml index 7619b8b..80f3444 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,6 @@ 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 } diff --git a/src/bin_image.rs b/src/bin_image/mod.rs similarity index 71% rename from src/bin_image.rs rename to src/bin_image/mod.rs index 896a7e2..3549c6d 100644 --- a/src/bin_image.rs +++ b/src/bin_image/mod.rs @@ -1,4 +1,5 @@ use crate::{UVec2, Vec2}; +pub mod neighbors; pub struct BinImage { data: Vec, @@ -74,19 +75,35 @@ 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::MIN && 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::MAX && self.get(UVec2::new(x - 1, y + 1)) { + neighbors |= neighbors::SOUTHWEST; + } + neighbors } /// Translates a point in positive (x, y) coordinates to a coordinate system centered at (0, 0). @@ -100,8 +117,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, ) } diff --git a/src/bin_image/neighbors.rs b/src/bin_image/neighbors.rs new file mode 100644 index 0000000..311d9cb --- /dev/null +++ b/src/bin_image/neighbors.rs @@ -0,0 +1,8 @@ +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; diff --git a/src/lib.rs b/src/lib.rs index ec37732..e5e6615 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,12 @@ #![doc = include_str!("../README.md")] +use crate::bin_image::BinImage; #[cfg(feature = "bevy")] pub use bevy_math::prelude::{UVec2, Vec2}; #[cfg(not(feature = "bevy"))] pub use glam::{UVec2, Vec2}; use std::fmt; - -use crate::{bin_image::BinImage, utils::points_to_drawing_order}; +use utils::{is_corner, match_neighbors}; mod bin_image; #[cfg(feature = "bevy")] @@ -62,24 +62,64 @@ 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 edge_points: Vec<_> = (0..image.height() * image.width()) + .map(|i| UVec2::new(i / image.height(), i % image.height())) + .filter(|p| image.get(*p) && is_corner(image.get_neighbors(*p))) .collect(); - points_to_drawing_order(edge_points) + let groups: Vec<_> = self + .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) + .map(|group| group.into_iter().map(|p| p.as_vec2()).collect()) + .collect(); + if translate { + groups + .into_iter() + .map(|group| self.translate(group)) + .collect() + } else { + groups + } + } + + /// 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. + fn points_to_drawing_order(&self, points: &[UVec2]) -> Vec> { + if points.is_empty() { + return Vec::new(); + } + + let mut groups: Vec> = Vec::new(); + let mut group: Vec<_> = Vec::new(); + let mut start = points[0]; + let mut current = start; + group.push(current); + + loop { + match_neighbors(self.image.get_neighbors(current), &mut current, &mut group); + + // 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 { + groups.push(group.clone()); + group.clear(); + + if let Some(new_start) = points + .iter() + .find(|p1| groups.iter().flatten().all(|p2| *p1 != p2)) + { + start = *new_start; + current = start; + group.push(current); } else { - group + break; } - }) - .collect() + } + } + + groups } /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). diff --git a/src/utils.rs b/src/utils.rs index 07cd18d..afb3a1b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,69 +1,199 @@ -use crate::{UVec2, Vec2}; -use std::collections::HashMap; +use crate::UVec2; +use std::cmp::Ordering::{Equal, Greater, Less}; +use std::ops::{AddAssign, SubAssign}; -// 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() +pub fn is_corner(neighbors: u8) -> bool { + !matches!( + neighbors, + 213 | 234 + | 200 + | 196 + | 194 + | 193 + | 192 + | 185 + | 118 + | 56 + | 52 + | 50 + | 49 + | 48 + | 177 + | 170 + | 184 + | 212 + | 209 + | 114 + | 116 + | 232 + | 226 + | 251 + | 255 + | 0 + ) } -/// 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(); - } - - let mut groups: Vec> = Vec::new(); - let mut group: Vec = Vec::new(); - let mut drawn_points_with_counts = HashMap::new(); - - let mut start = points[0]; - let mut current = start; - group.push(current); - drawn_points_with_counts.insert(current, 2); - - 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); - } +#[allow(clippy::too_many_lines)] +pub fn match_neighbors(neighbors: u8, current: &mut UVec2, group: &mut Vec) { + if let Some(last) = group.last() { + let last = *last; + if last != *current && is_corner(neighbors) { + group.push(*current); } - - // 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; + println!("l:{last}"); + println!("c:{current}"); + println!("n:{neighbors}"); + // println!("{group:#?}"); + if current.x == 511 { + todo!(); + } + match neighbors { + 189 | 191 | 119 | 126 | 187 | 185 | 118 | 56 | 52 | 50 | 49 | 48 => { + match last.x.cmp(¤t.x) { + Greater | Equal => current.x.sub_assign(1), + Less => current.x.add_assign(1), + } } - - 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; + 221 | 215 | 238 | 235 | 213 | 234 | 200 | 196 | 194 | 193 | 192 => { + match last.y.cmp(¤t.y) { + Greater | Equal => current.y.sub_assign(1), + Less => current.y.add_assign(1), + } } + 168 | 160 => match last.x.cmp(¤t.x) { + Greater => current.y.add_assign(1), + Equal => current.x.add_assign(1), + Less => unreachable!(), + }, + 145 | 144 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x.sub_assign(1), + Less => current.y.add_assign(1), + }, + 84 | 80 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x.sub_assign(1), + Less => current.y.sub_assign(1), + }, + 98 | 96 => match last.x.cmp(¤t.x) { + Greater => current.y.sub_assign(1), + Equal => current.x.add_assign(1), + Less => unreachable!(), + }, + 177 => match last.x.cmp(¤t.x) { + Greater => { + group.push(*current); + current.y += 1; + } + Equal => unreachable!(), + Less => current.x.add_assign(1), + }, + 184 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => { + group.push(*current); + current.x -= 1; + } + Less => current.x.add_assign(1), + }, + 212 => match last.y.cmp(¤t.y) { + Greater => { + group.push(*current); + current.x -= 1; + } + Equal => unreachable!(), + Less => current.y.add_assign(1), + }, + 209 => match last.y.cmp(¤t.y) { + Greater => unreachable!(), + Equal => { + group.push(*current); + current.y -= 1; + } + Less => current.y.add_assign(1), + }, + 114 => match last.x.cmp(¤t.x) { + Greater => current.x.sub_assign(1), + Equal => unreachable!(), + Less => { + group.push(*current); + current.y -= 1; + } + }, + 116 => match last.x.cmp(¤t.x) { + Greater => current.x.sub_assign(1), + Equal => { + group.push(*current); + current.x += 1; + } + Less => unreachable!(), + }, + 232 => match last.y.cmp(¤t.y) { + Greater => current.y.sub_assign(1), + Equal => unreachable!(), + Less => { + group.push(*current); + current.x += 1; + } + }, + 226 => match last.y.cmp(¤t.y) { + Greater => current.y.sub_assign(1), + Equal => { + group.push(*current); + current.y -= 1; + } + Less => unreachable!(), + }, + 169..=171 | 253 => match last.x.cmp(¤t.x) { + Greater | Less => unreachable!(), + Equal => { + group.push(*current); + current.x += 1; + } + }, + 153 | 157 | 149 => match last.x.cmp(¤t.x) { + Greater | Equal => unreachable!(), + Less => { + group.push(*current); + current.y += 1; + } + }, + 85..=87 | 254 => match last.x.cmp(¤t.x) { + Greater | Less => unreachable!(), + Equal => { + group.push(*current); + current.x -= 1; + } + }, + 251 => match last.x.cmp(¤t.x) { + Greater | Equal => unreachable!(), + Less => { + group.push(*current); + current.y -= 1; + } + }, + 247 => match last.x.cmp(¤t.x) { + Greater => { + group.push(*current); + current.y += 1; + } + Equal | Less => unreachable!(), + }, + 110 | 102 => match last.x.cmp(¤t.x) { + Greater => { + group.push(*current); + current.y -= 1; + } + Equal | Less => unreachable!(), + }, + 106 => match last.x.cmp(¤t.x) { + Greater | Equal => { + group.push(*current); + current.y -= 1; + } + Less => unreachable!(), + }, + _ => {} } } - groups.push(group); - - groups } From b1dbde757ed71e94dcbdb4dd6520775b61bb1bcc Mon Sep 17 00:00:00 2001 From: salam Date: Wed, 30 Oct 2024 19:50:55 +0900 Subject: [PATCH 19/32] fixed get method of `BinImage` --- src/bin_image/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin_image/mod.rs b/src/bin_image/mod.rs index 3549c6d..8dd736b 100644 --- a/src/bin_image/mod.rs +++ b/src/bin_image/mod.rs @@ -61,7 +61,7 @@ impl BinImage { .copied() { byte >>= index % 8; // index of bit - x <= self.width && byte & 1 > 0 + x < self.width && byte & 1 > 0 } else { false } From 0eca991f382c3a1a5ba3a9c66a62122f70325b3b Mon Sep 17 00:00:00 2001 From: salam Date: Wed, 30 Oct 2024 20:09:38 +0900 Subject: [PATCH 20/32] modified: src/utils.rs --- src/utils.rs | 62 ++++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index afb3a1b..0322d66 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,13 +5,22 @@ use std::ops::{AddAssign, SubAssign}; pub fn is_corner(neighbors: u8) -> bool { !matches!( neighbors, - 213 | 234 + 238 | 235 + | 234 + | 221 + | 215 + | 213 | 200 | 196 | 194 | 193 | 192 + | 191 + | 189 + | 187 | 185 + | 126 + | 119 | 118 | 56 | 52 @@ -44,17 +53,14 @@ pub fn match_neighbors(neighbors: u8, current: &mut UVec2, group: &mut Vec { + 191 | 189 | 187 | 185 | 126 | 119 | 118 | 56 | 52 | 48..=50 => { match last.x.cmp(¤t.x) { Greater | Equal => current.x.sub_assign(1), Less => current.x.add_assign(1), } } - 221 | 215 | 238 | 235 | 213 | 234 | 200 | 196 | 194 | 193 | 192 => { + 238 | 235 | 234 | 221 | 215 | 213 | 200 | 196 | 192..=194 => { match last.y.cmp(¤t.y) { Greater | Equal => current.y.sub_assign(1), Less => current.y.add_assign(1), @@ -80,6 +86,27 @@ pub fn match_neighbors(neighbors: u8, current: &mut UVec2, group: &mut Vec current.x.add_assign(1), Less => unreachable!(), }, + 169..=171 | 253 => match last.x.cmp(¤t.x) { + Greater | Less => unreachable!(), + Equal => { + group.push(*current); + current.x += 1; + } + }, + 85..=87 | 254 => match last.x.cmp(¤t.x) { + Greater | Less => unreachable!(), + Equal => { + group.push(*current); + current.x -= 1; + } + }, + 153 | 157 | 149 => match last.x.cmp(¤t.x) { + Greater | Equal => unreachable!(), + Less => { + group.push(*current); + current.y += 1; + } + }, 177 => match last.x.cmp(¤t.x) { Greater => { group.push(*current); @@ -99,7 +126,7 @@ pub fn match_neighbors(neighbors: u8, current: &mut UVec2, group: &mut Vec match last.y.cmp(¤t.y) { Greater => { group.push(*current); - current.x -= 1; + current.x -= 1; } Equal => unreachable!(), Less => current.y.add_assign(1), @@ -144,27 +171,6 @@ pub fn match_neighbors(neighbors: u8, current: &mut UVec2, group: &mut Vec unreachable!(), }, - 169..=171 | 253 => match last.x.cmp(¤t.x) { - Greater | Less => unreachable!(), - Equal => { - group.push(*current); - current.x += 1; - } - }, - 153 | 157 | 149 => match last.x.cmp(¤t.x) { - Greater | Equal => unreachable!(), - Less => { - group.push(*current); - current.y += 1; - } - }, - 85..=87 | 254 => match last.x.cmp(¤t.x) { - Greater | Less => unreachable!(), - Equal => { - group.push(*current); - current.x -= 1; - } - }, 251 => match last.x.cmp(¤t.x) { Greater | Equal => unreachable!(), Less => { From c4ca604e3cde1a40dffc8d92b2dd378b951335f9 Mon Sep 17 00:00:00 2001 From: salam Date: Thu, 31 Oct 2024 20:58:25 +0900 Subject: [PATCH 21/32] reworking edges search algoritm --- examples/bevy-image.rs | 4 +- src/lib.rs | 58 +++---- src/utils.rs | 354 +++++++++++++++++++++-------------------- 3 files changed, 199 insertions(+), 217 deletions(-) diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index a24cba2..218a169 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -48,9 +48,9 @@ fn draw_png(image: Image, img_path: &str) { // 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/src/lib.rs b/src/lib.rs index e5e6615..84b05e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ pub use bevy_math::prelude::{UVec2, Vec2}; #[cfg(not(feature = "bevy"))] pub use glam::{UVec2, Vec2}; use std::fmt; -use utils::{is_corner, match_neighbors}; +use utils::is_corner; mod bin_image; #[cfg(feature = "bevy")] @@ -62,64 +62,42 @@ 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: Vec<_> = (0..image.height() * image.width()) + let corners: Vec<_> = (0..image.height() * image.width()) .map(|i| UVec2::new(i / image.height(), i % image.height())) .filter(|p| image.get(*p) && is_corner(image.get_neighbors(*p))) .collect(); - let groups: Vec<_> = self - .points_to_drawing_order(&edge_points) + let objects: Vec<_> = self + .collect_objects(&corners) .into_iter() - .map(|group| group.into_iter().map(|p| p.as_vec2()).collect()) + .map(|object| object.into_iter().map(|p| p.as_vec2()).collect()) .collect(); if translate { - groups + objects .into_iter() .map(|group| self.translate(group)) .collect() } else { - groups + objects } } - /// 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. - fn points_to_drawing_order(&self, points: &[UVec2]) -> Vec> { - if points.is_empty() { + fn collect_objects(&self, corners: &[UVec2]) -> Vec> { + if corners.is_empty() { return Vec::new(); } - let mut groups: Vec> = Vec::new(); - let mut group: Vec<_> = Vec::new(); - let mut start = points[0]; - let mut current = start; - group.push(current); - - loop { - match_neighbors(self.image.get_neighbors(current), &mut current, &mut group); - - // 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 { - groups.push(group.clone()); - group.clear(); - - if let Some(new_start) = points - .iter() - .find(|p1| groups.iter().flatten().all(|p2| *p1 != p2)) - { - start = *new_start; - current = start; - group.push(current); - } else { - break; - } - } + let mut objects: Vec> = Vec::new(); + + while let Some(start) = corners + .iter() + .find(|point| objects.iter().all(|object| !object.contains(point))) + { + let object = self.collect_object(*start); + objects.push(object); } - groups + objects } /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). diff --git a/src/utils.rs b/src/utils.rs index 0322d66..de199c8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,205 +1,209 @@ -use crate::UVec2; +use crate::{Edges, UVec2}; use std::cmp::Ordering::{Equal, Greater, Less}; use std::ops::{AddAssign, SubAssign}; pub fn is_corner(neighbors: u8) -> bool { !matches!( neighbors, - 238 | 235 - | 234 + 255 + | 239 + | 238 + | 232..=235 + | 127 + | 226 + | 223 | 221 | 215 | 213 + | 212 + | 209 + | 207 + | 201 | 200 - | 196 - | 194 - | 193 - | 192 - | 191 + | 196..=198 + | 191..=194 | 189 | 187 | 185 + | 184 + | 177 | 126 | 119 | 118 - | 56 - | 52 - | 50 - | 49 - | 48 - | 177 - | 170 - | 184 - | 212 - | 209 - | 114 | 116 - | 232 - | 226 - | 251 - | 255 + | 114 + | 58 + | 56 + | 52..=54 + | 48..=50 | 0 ) } -#[allow(clippy::too_many_lines)] -pub fn match_neighbors(neighbors: u8, current: &mut UVec2, group: &mut Vec) { - if let Some(last) = group.last() { - let last = *last; - if last != *current && is_corner(neighbors) { - group.push(*current); - } - println!("l:{last}"); - println!("c:{current}"); - println!("n:{neighbors}"); - // println!("{group:#?}"); - match neighbors { - 191 | 189 | 187 | 185 | 126 | 119 | 118 | 56 | 52 | 48..=50 => { - match last.x.cmp(¤t.x) { +impl Edges { + #[allow(clippy::too_many_lines)] + pub(super) fn collect_object(&self, mut current: UVec2) -> Vec { + let mut group: Vec = Vec::new(); + group.push(current); + loop { + let (last, neighbors) = (*group.last().unwrap(), self.image.get_neighbors(current)); + if last != current && is_corner(neighbors) { + group.push(current); + } + match neighbors { + 253 | 169..=171 | 32 => current.x.add_assign(1), + 254 | 85..=87 | 16 => current.x.sub_assign(1), + 251 | 110 | 106 | 102 | 64 => current.y.sub_assign(1), + 247 | 153 | 157 | 149 | 128 => current.y.add_assign(1), + 233 + | 191 + | 189 + | 187 + | 185 + | 127 + | 126 + | 119 + | 118 + | 58 + | 56 + | 52..=54 + | 48..=50 => match last.x.cmp(¤t.x) { Greater | Equal => current.x.sub_assign(1), Less => current.x.add_assign(1), - } - } - 238 | 235 | 234 | 221 | 215 | 213 | 200 | 196 | 192..=194 => { - match last.y.cmp(¤t.y) { + }, + 239 + | 238 + | 235 + | 234 + | 223 + | 221 + | 215 + | 213 + | 207 + | 201 + | 200 + | 196..=198 + | 192..=194 => match last.y.cmp(¤t.y) { Greater | Equal => current.y.sub_assign(1), Less => current.y.add_assign(1), + }, + 168 | 160 => match last.x.cmp(¤t.x) { + Greater => current.y.add_assign(1), + Equal => current.x.add_assign(1), + Less => unreachable!(), + }, + 145 | 144 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x.sub_assign(1), + Less => current.y.add_assign(1), + }, + 84 | 80 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x.sub_assign(1), + Less => current.y.sub_assign(1), + }, + 98 | 96 => match last.x.cmp(¤t.x) { + Greater => current.y.sub_assign(1), + Equal => current.x.add_assign(1), + Less => unreachable!(), + }, + 177 => match last.x.cmp(¤t.x) { + Greater => { + group.push(current); + current.y += 1; + } + Equal => unreachable!(), + Less => current.x.add_assign(1), + }, + 184 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => { + group.push(current); + current.x -= 1; + } + Less => current.x.add_assign(1), + }, + 212 => match last.y.cmp(¤t.y) { + Greater => { + group.push(current); + current.x -= 1; + } + Equal => unreachable!(), + Less => current.y.add_assign(1), + }, + 209 => match last.y.cmp(¤t.y) { + Greater => unreachable!(), + Equal => { + group.push(current); + current.y -= 1; + } + Less => current.y.add_assign(1), + }, + 114 => match last.x.cmp(¤t.x) { + Greater => current.x.sub_assign(1), + Equal => unreachable!(), + Less => { + group.push(current); + current.y -= 1; + } + }, + 116 => match last.x.cmp(¤t.x) { + Greater => current.x.sub_assign(1), + Equal => { + group.push(current); + current.x += 1; + } + Less => unreachable!(), + }, + 232 => match last.y.cmp(¤t.y) { + Greater => current.y.sub_assign(1), + Equal => unreachable!(), + Less => { + group.push(current); + current.x += 1; + } + }, + 226 => match last.y.cmp(¤t.y) { + Greater => current.y.sub_assign(1), + Equal => { + group.push(current); + current.y -= 1; + } + Less => unreachable!(), + }, + 240 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { + (Less, Equal) => current.y.sub_assign(1), + (Equal, Less) => current.x.add_assign(1), + (Greater, Equal) => current.y.add_assign(1), + (Equal, Greater) => current.x.sub_assign(1), + _ => unreachable!(), + }, + 249 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x.add_assign(1), + Less => current.y.sub_assign(1), + }, + 112 => match last.x.cmp(¤t.x) { + Greater => current.x.sub_assign(1), + Equal => current.x.add_assign(1), + Less => current.y.sub_assign(1), + }, + 246 => match last.x.cmp(¤t.x) { + Greater => current.y.add_assign(1), + Equal => current.x.sub_assign(1), + Less => current.x.add_assign(1), + }, + 0 | 255 => unreachable!(), + _ => { + println!("l:{last}"); + println!("c:{current}"); + println!("n:{neighbors}"); + unreachable!() } } - 168 | 160 => match last.x.cmp(¤t.x) { - Greater => current.y.add_assign(1), - Equal => current.x.add_assign(1), - Less => unreachable!(), - }, - 145 | 144 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => current.x.sub_assign(1), - Less => current.y.add_assign(1), - }, - 84 | 80 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => current.x.sub_assign(1), - Less => current.y.sub_assign(1), - }, - 98 | 96 => match last.x.cmp(¤t.x) { - Greater => current.y.sub_assign(1), - Equal => current.x.add_assign(1), - Less => unreachable!(), - }, - 169..=171 | 253 => match last.x.cmp(¤t.x) { - Greater | Less => unreachable!(), - Equal => { - group.push(*current); - current.x += 1; - } - }, - 85..=87 | 254 => match last.x.cmp(¤t.x) { - Greater | Less => unreachable!(), - Equal => { - group.push(*current); - current.x -= 1; - } - }, - 153 | 157 | 149 => match last.x.cmp(¤t.x) { - Greater | Equal => unreachable!(), - Less => { - group.push(*current); - current.y += 1; - } - }, - 177 => match last.x.cmp(¤t.x) { - Greater => { - group.push(*current); - current.y += 1; - } - Equal => unreachable!(), - Less => current.x.add_assign(1), - }, - 184 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => { - group.push(*current); - current.x -= 1; - } - Less => current.x.add_assign(1), - }, - 212 => match last.y.cmp(¤t.y) { - Greater => { - group.push(*current); - current.x -= 1; - } - Equal => unreachable!(), - Less => current.y.add_assign(1), - }, - 209 => match last.y.cmp(¤t.y) { - Greater => unreachable!(), - Equal => { - group.push(*current); - current.y -= 1; - } - Less => current.y.add_assign(1), - }, - 114 => match last.x.cmp(¤t.x) { - Greater => current.x.sub_assign(1), - Equal => unreachable!(), - Less => { - group.push(*current); - current.y -= 1; - } - }, - 116 => match last.x.cmp(¤t.x) { - Greater => current.x.sub_assign(1), - Equal => { - group.push(*current); - current.x += 1; - } - Less => unreachable!(), - }, - 232 => match last.y.cmp(¤t.y) { - Greater => current.y.sub_assign(1), - Equal => unreachable!(), - Less => { - group.push(*current); - current.x += 1; - } - }, - 226 => match last.y.cmp(¤t.y) { - Greater => current.y.sub_assign(1), - Equal => { - group.push(*current); - current.y -= 1; - } - Less => unreachable!(), - }, - 251 => match last.x.cmp(¤t.x) { - Greater | Equal => unreachable!(), - Less => { - group.push(*current); - current.y -= 1; - } - }, - 247 => match last.x.cmp(¤t.x) { - Greater => { - group.push(*current); - current.y += 1; - } - Equal | Less => unreachable!(), - }, - 110 | 102 => match last.x.cmp(¤t.x) { - Greater => { - group.push(*current); - current.y -= 1; - } - Equal | Less => unreachable!(), - }, - 106 => match last.x.cmp(¤t.x) { - Greater | Equal => { - group.push(*current); - current.y -= 1; - } - Less => unreachable!(), - }, - _ => {} + if current == group[0] { + break group; + } } } } From 2bbb2be083cb85275f32fcd927fd55519cd61c2b Mon Sep 17 00:00:00 2001 From: salam Date: Fri, 1 Nov 2024 18:53:38 +0900 Subject: [PATCH 22/32] added new image for tests --- assets/broken-square.png | Bin 0 -> 296 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/broken-square.png diff --git a/assets/broken-square.png b/assets/broken-square.png new file mode 100644 index 0000000000000000000000000000000000000000..dd2a5c0f4d40d922898e97485b5667cfdb810c77 GIT binary patch literal 296 zcmV+@0oVSCP)Px#;z>k7R9J=OS3wd3Aqaz={{PGLl9SsNz~Bk!f-C&W75Y z0UBH3-2%4wdI3yvF@c~`2O!01=K)!fi%5CvMjlul%aQ$CVzTj#$SYysDnS)Ybnh;3 zxpun%?Um_9bax?|Jn->Kh`tk^Txh2x)J3O~ct;DcNr=q&0W0XGXkF0&oo&Hk>pgBF uVSa}p$-2{<+*`TjhrSE43Eg_U{~n&c=rKA7{t{0B0000 Date: Fri, 1 Nov 2024 18:55:37 +0900 Subject: [PATCH 23/32] fix: inner edges collecting --- src/lib.rs | 28 +++- src/utils.rs | 439 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 297 insertions(+), 170 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 84b05e6..746f721 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ pub use bevy_math::prelude::{UVec2, Vec2}; #[cfg(not(feature = "bevy"))] pub use glam::{UVec2, Vec2}; use std::fmt; -use utils::is_corner; +use utils::{handle_neighbors, in_polygon, is_corner}; mod bin_image; #[cfg(feature = "bevy")] @@ -89,10 +89,11 @@ impl Edges { let mut objects: Vec> = Vec::new(); - while let Some(start) = corners - .iter() - .find(|point| objects.iter().all(|object| !object.contains(point))) - { + while let Some(start) = corners.iter().find(|point| { + objects + .iter() + .all(|object| !object.contains(point) && !in_polygon(**point, object)) + }) { let object = self.collect_object(*start); objects.push(object); } @@ -100,6 +101,23 @@ impl Edges { objects } + fn collect_object(&self, mut current: UVec2) -> Vec { + let mut group: Vec = Vec::new(); + group.push(current); + loop { + let (last, neighbors) = (*group.last().unwrap(), self.image.get_neighbors(current)); + if last != current && is_corner(neighbors) { + group.push(current); + } + if let Some(point) = handle_neighbors(&mut current, last, neighbors) { + group.push(point); + } + if current == group[0] { + break group; + } + } + } + /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). /// /// # Arguments diff --git a/src/utils.rs b/src/utils.rs index de199c8..c6f783d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,41 @@ -use crate::{Edges, UVec2}; +use crate::UVec2; use std::cmp::Ordering::{Equal, Greater, Less}; -use std::ops::{AddAssign, SubAssign}; + +pub fn in_polygon(point: UVec2, polygon: &[UVec2]) -> bool { + let mut is_inside = false; + for win in polygon.windows(2) { + let (p1, p2) = (win[0], win[1]); + if (p1.x.min(p2.x) <= point.x && point.x <= p1.x.max(p2.x)) + && (p1.y.min(p2.y) <= point.y && point.y <= p1.y.max(p2.y)) + && (p1.y.max(p2.y) - p1.y.min(p2.y)) * (point.x - p1.x.min(p2.x)) + == (p2.x.max(p2.x) - p1.x.min(p2.x)) * (point.y - p1.y.min(p2.y)) + { + return true; + } + + if p1.y <= point.y && point.y < p2.y || p2.y <= point.y && point.y < p1.y { + let (point_x, offset_x) = point + .x + .checked_sub(p1.x) + .map_or_else(|| (0, p1.x - point.x), |dx| (dx, 0)); + let (point_y, offset_y) = point + .y + .checked_sub(p1.y) + .map_or_else(|| (0, p1.y - point.y), |dy| (dy, 0)); + + let (dx, offset_x) = (p2.x + offset_x) + .checked_sub(p1.x) + .map_or_else(|| (0, p1.x - p2.x - offset_x), |dx| (dx, 0)); + let (dy, offset_y) = (p2.y + offset_y) + .checked_sub(p1.y) + .map_or_else(|| (0, p1.y - p2.y - offset_y), |dy| (dy, 0)); + if (point_x + offset_x) * dy >= dx * (point_y + offset_y) { + is_inside = !is_inside; + } + } + } + is_inside +} pub fn is_corner(neighbors: u8) -> bool { !matches!( @@ -40,170 +75,244 @@ pub fn is_corner(neighbors: u8) -> bool { ) } -impl Edges { - #[allow(clippy::too_many_lines)] - pub(super) fn collect_object(&self, mut current: UVec2) -> Vec { - let mut group: Vec = Vec::new(); - group.push(current); - loop { - let (last, neighbors) = (*group.last().unwrap(), self.image.get_neighbors(current)); - if last != current && is_corner(neighbors) { - group.push(current); - } - match neighbors { - 253 | 169..=171 | 32 => current.x.add_assign(1), - 254 | 85..=87 | 16 => current.x.sub_assign(1), - 251 | 110 | 106 | 102 | 64 => current.y.sub_assign(1), - 247 | 153 | 157 | 149 | 128 => current.y.add_assign(1), - 233 - | 191 - | 189 - | 187 - | 185 - | 127 - | 126 - | 119 - | 118 - | 58 - | 56 - | 52..=54 - | 48..=50 => match last.x.cmp(¤t.x) { - Greater | Equal => current.x.sub_assign(1), - Less => current.x.add_assign(1), - }, - 239 - | 238 - | 235 - | 234 - | 223 - | 221 - | 215 - | 213 - | 207 - | 201 - | 200 - | 196..=198 - | 192..=194 => match last.y.cmp(¤t.y) { - Greater | Equal => current.y.sub_assign(1), - Less => current.y.add_assign(1), - }, - 168 | 160 => match last.x.cmp(¤t.x) { - Greater => current.y.add_assign(1), - Equal => current.x.add_assign(1), - Less => unreachable!(), - }, - 145 | 144 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => current.x.sub_assign(1), - Less => current.y.add_assign(1), - }, - 84 | 80 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => current.x.sub_assign(1), - Less => current.y.sub_assign(1), - }, - 98 | 96 => match last.x.cmp(¤t.x) { - Greater => current.y.sub_assign(1), - Equal => current.x.add_assign(1), - Less => unreachable!(), - }, - 177 => match last.x.cmp(¤t.x) { - Greater => { - group.push(current); - current.y += 1; - } - Equal => unreachable!(), - Less => current.x.add_assign(1), - }, - 184 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => { - group.push(current); - current.x -= 1; - } - Less => current.x.add_assign(1), - }, - 212 => match last.y.cmp(¤t.y) { - Greater => { - group.push(current); - current.x -= 1; - } - Equal => unreachable!(), - Less => current.y.add_assign(1), - }, - 209 => match last.y.cmp(¤t.y) { - Greater => unreachable!(), - Equal => { - group.push(current); - current.y -= 1; - } - Less => current.y.add_assign(1), - }, - 114 => match last.x.cmp(¤t.x) { - Greater => current.x.sub_assign(1), - Equal => unreachable!(), - Less => { - group.push(current); - current.y -= 1; - } - }, - 116 => match last.x.cmp(¤t.x) { - Greater => current.x.sub_assign(1), - Equal => { - group.push(current); - current.x += 1; - } - Less => unreachable!(), - }, - 232 => match last.y.cmp(¤t.y) { - Greater => current.y.sub_assign(1), - Equal => unreachable!(), - Less => { - group.push(current); - current.x += 1; - } - }, - 226 => match last.y.cmp(¤t.y) { - Greater => current.y.sub_assign(1), - Equal => { - group.push(current); - current.y -= 1; - } - Less => unreachable!(), - }, - 240 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { - (Less, Equal) => current.y.sub_assign(1), - (Equal, Less) => current.x.add_assign(1), - (Greater, Equal) => current.y.add_assign(1), - (Equal, Greater) => current.x.sub_assign(1), - _ => unreachable!(), - }, - 249 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => current.x.add_assign(1), - Less => current.y.sub_assign(1), - }, - 112 => match last.x.cmp(¤t.x) { - Greater => current.x.sub_assign(1), - Equal => current.x.add_assign(1), - Less => current.y.sub_assign(1), - }, - 246 => match last.x.cmp(¤t.x) { - Greater => current.y.add_assign(1), - Equal => current.x.sub_assign(1), - Less => current.x.add_assign(1), - }, - 0 | 255 => unreachable!(), - _ => { - println!("l:{last}"); - println!("c:{current}"); - println!("n:{neighbors}"); - unreachable!() - } - } - if current == group[0] { - break group; +#[allow(clippy::too_many_lines)] +pub fn handle_neighbors(current: &mut UVec2, last: UVec2, neighbors: u8) -> Option { + println!("l:{last}"); + println!("c:{current}"); + println!("n:{neighbors}"); + match neighbors { + 253 | 169..=171 | 40 | 38 | 32 => current.x += 1, + 254 | 85..=87 | 16 => current.x -= 1, + 251 | 110 | 106 | 102 | 64 => current.y -= 1, + 247 | 153 | 157 | 149 | 129 | 128 => current.y += 1, + 233 | 191 | 189 | 187 | 185 | 127 | 126 | 119 | 118 | 58 | 56 | 52..=54 | 48..=50 => { + match last.x.cmp(¤t.x) { + Greater | Equal => current.x -= 1, + Less => current.x += 1, + } + } + 239 | 238 | 235 | 234 | 223 | 221 | 215 | 213 | 207 | 201 | 200 | 196..=198 | 192..=194 => { + match last.y.cmp(¤t.y) { + Greater | Equal => current.y -= 1, + Less => current.y += 1, + } + } + 168 | 162 | 160 => match last.x.cmp(¤t.x) { + Greater => current.y += 1, + Equal => current.x += 1, + Less => unreachable!(), + }, + 145 | 144 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x -= 1, + Less => current.y += 1, + }, + 90 | 84 | 80 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x -= 1, + Less => current.y -= 1, + }, + 98 | 96 => match last.x.cmp(¤t.x) { + Greater => current.y -= 1, + Equal => current.x += 1, + Less => unreachable!(), + }, + 177 => match last.x.cmp(¤t.x) { + Greater => { + current.y += 1; + return Some(current.with_y(current.y - 1)); + } + Equal => unreachable!(), + Less => current.x += 1, + }, + 184 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => { + current.x -= 1; + return Some(current.with_x(current.x + 1)); + } + Less => current.x += 1, + }, + 212 => match last.y.cmp(¤t.y) { + Greater => { + current.x -= 1; + return Some(current.with_x(current.x + 1)); + } + Equal => unreachable!(), + Less => current.y += 1, + }, + 209 => match last.y.cmp(¤t.y) { + Greater => unreachable!(), + Equal => { + current.y -= 1; + return Some(current.with_y(current.y + 1)); + } + Less => current.y += 1, + }, + 114 => match last.x.cmp(¤t.x) { + Greater => current.x -= 1, + Equal => unreachable!(), + Less => { + current.y -= 1; + return Some(current.with_y(current.y + 1)); + } + }, + 116 => match last.x.cmp(¤t.x) { + Greater => current.x -= 1, + Equal => { + current.x += 1; + return Some(current.with_x(current.x - 1)); + } + Less => unreachable!(), + }, + 232 => match last.y.cmp(¤t.y) { + Greater => current.y -= 1, + Equal => unreachable!(), + Less => { + current.x += 1; + return Some(current.with_x(current.x - 1)); + } + }, + 226 => match last.y.cmp(¤t.y) { + Greater => current.y -= 1, + Equal => { + current.y -= 1; + return Some(current.with_y(current.y + 1)); + } + Less => unreachable!(), + }, + 240 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { + (Less, Equal) => current.y -= 1, + (Equal, Less) => current.x += 1, + (Greater, Equal) => current.y += 1, + (Equal, Greater) => current.x -= 1, + _ => unreachable!(), + }, + 249 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x += 1, + Less => current.y -= 1, + }, + 115 | 112 => match last.x.cmp(¤t.x) { + Greater => current.x -= 1, + Equal => current.x += 1, + Less => current.y -= 1, + }, + 246 => match last.x.cmp(¤t.x) { + Greater => current.y += 1, + Equal => current.x -= 1, + Less => current.x += 1, + }, + 186 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => current.x -= 1, + Less => { + current.x += 1; + return Some(current.with_x(current.x - 1)); + } + }, + 231 => match last.x.cmp(¤t.x) { + Greater => current.y += 1, + Equal => current.y -= 1, + Less => unreachable!(), + }, + 8 => { + current.x += 1; + current.y += 1; + } + 12 => match last.x.cmp(¤t.x) { + Greater => { + current.x -= 1; + current.y -= 1; + } + Equal => unreachable!(), + Less => { + current.x += 1; + current.y += 1; + } + }, + 44 => match last.x.cmp(¤t.x) { + Greater => { + current.x -= 1; + current.y -= 1; + } + Equal => unreachable!(), + Less => current.x += 1, + }, + 24 => match last.x.cmp(¤t.x) { + Greater => current.x -= 1, + Equal => unreachable!(), + Less => { + current.x += 1; + current.y += 1; + } + }, + 132 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => { + current.x -= 1; + current.y -= 1; + } + Less => current.y += 1, + }, + 103 => match last.x.cmp(¤t.x) { + Greater => { + current.x -= 1; + current.y += 1; + } + Equal => unreachable!(), + Less => current.y -= 1, + }, + 2 => { + current.x += 1; + current.y -= 1; + } + 67 | 65 => match last.x.cmp(¤t.x) { + Greater => unreachable!(), + Equal => { + current.x -= 1; + current.y += 1; + } + Less => current.y -= 1, + }, + 3 => match last.x.cmp(¤t.x) { + Greater => { + current.x -= 1; + current.y += 1; + } + Equal => unreachable!(), + Less => { + current.x += 1; + current.y -= 1; + } + }, + 22 | 18 => match last.x.cmp(¤t.x) { + Greater => current.x -= 1, + Equal => unreachable!(), + Less => { + current.x += 1; + current.y -= 1; + } + }, + 7 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { + (Greater, Less) => { + current.x -= 1; + current.y += 1; + } + (Less, Greater) => { + current.x -= 1; + current.y -= 1; + } + (Less, Less) => { + current.x += 1; + current.y -= 1; } + _ => unreachable!(), + }, + 0 | 255 => unreachable!(), + _ => { + unreachable!() } } + None } From ac0c3ddcde59c036fa55c59e3a9880d77e348ae2 Mon Sep 17 00:00:00 2001 From: salam Date: Fri, 1 Nov 2024 21:18:33 +0900 Subject: [PATCH 24/32] added more handlers --- src/utils.rs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index c6f783d..d82eabe 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -69,23 +69,19 @@ pub fn is_corner(neighbors: u8) -> bool { | 114 | 58 | 56 - | 52..=54 - | 48..=50 + | 48..=54 | 0 ) } #[allow(clippy::too_many_lines)] pub fn handle_neighbors(current: &mut UVec2, last: UVec2, neighbors: u8) -> Option { - println!("l:{last}"); - println!("c:{current}"); - println!("n:{neighbors}"); match neighbors { 253 | 169..=171 | 40 | 38 | 32 => current.x += 1, 254 | 85..=87 | 16 => current.x -= 1, 251 | 110 | 106 | 102 | 64 => current.y -= 1, 247 | 153 | 157 | 149 | 129 | 128 => current.y += 1, - 233 | 191 | 189 | 187 | 185 | 127 | 126 | 119 | 118 | 58 | 56 | 52..=54 | 48..=50 => { + 233 | 191 | 189 | 187 | 185 | 127 | 126 | 119 | 118 | 58 | 56 | 48..=54 => { match last.x.cmp(¤t.x) { Greater | Equal => current.x -= 1, Less => current.x += 1, @@ -107,7 +103,7 @@ pub fn handle_neighbors(current: &mut UVec2, last: UVec2, neighbors: u8) -> Opti Equal => current.x -= 1, Less => current.y += 1, }, - 90 | 84 | 80 => match last.x.cmp(¤t.x) { + 90 | 84 | 82 | 80 => match last.x.cmp(¤t.x) { Greater => unreachable!(), Equal => current.x -= 1, Less => current.y -= 1, @@ -309,9 +305,30 @@ pub fn handle_neighbors(current: &mut UVec2, last: UVec2, neighbors: u8) -> Opti } _ => unreachable!(), }, + 36 => match last.x.cmp(¤t.x) { + Greater => { + current.x -= 1; + current.y -= 1; + } + Equal => unreachable!(), + Less => current.x += 1, + }, + 74 => match last.x.cmp(¤t.x) { + Greater => current.y -= 1, + Equal => { + current.x += 1; + current.y += 1; + } + Less => unreachable!(), + } + 250 => match last.y.cmp(¤t.y) { + Greater => current.x -= 1, + Equal => current.y -= 1, + Less => current.y += 1, + } 0 | 255 => unreachable!(), _ => { - unreachable!() + todo!("\nadd handle for this case:\nlast point:{last}\ncurrent point:{current}\ncurrent neighbors:{neighbors}") } } None From 27542a311e5d7f4a29adea40f69a7617739b682f Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 10 Nov 2024 09:54:07 +0900 Subject: [PATCH 25/32] add: new image for test --- assets/diagonals.png | Bin 0 -> 1108 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/diagonals.png diff --git a/assets/diagonals.png b/assets/diagonals.png new file mode 100644 index 0000000000000000000000000000000000000000..d6307147e4da6f2704f749896d0012717d06f50e GIT binary patch literal 1108 zcmV-a1grarP)Px(4oO5oRCt{2oJ&^ZFbqWlht04NHpQ4tu@RO-PdXe^_$TX0dUA3neW+AyeQa5l z-E|yBe7%2uTeZ#3s_tesyRtJ*zTQ8-C3d)-;l#)uUB_vSc{`&{%jmVwo@!ru{doKH z`AnxP?^&ZABuJe~8;R^CRU-S$IOX@yL%O#7dJ*|OZ)W6rG*0Usl#VlfO*gReJd#z} zXJb7=o~7AhJ!-7z$tQiL(RIe?YNxO}<2ZB!38k-eA0q0egI_MkL+HrcyG&y17VDy~Vbi3Pee z@jta)JDZFrCZ}PwzoTF}F?1vA=5b55jlUVme$2ZS=!+1y(-c@3kM1a)( zYZ^h7q|q~tpv1s!xaHJWkj@^xovybmR?MzZ6goeaty|A_py!w{U2CQ4D>>yj_Q!WMqk1(ODvKG&3=O_jGcHm z#WHhCXYfd@Q)nw^Y_*2W?v@dx+RS=SSEdamiyXi35{k(Yv@l%f$xNCx#B+VvhHPmo zBeXMwVn-VQokOnbh-qfS#g)QJIB9NJT#3=*dOIWt%A?6e80BvM7=MeIAK=eKQ~?q$ z3)NZi0=>IqR{bEpH*7@H(8{}rGZgQzJ6e08eS13PaaOL((x}9wW+!-msA<2FR<9_> zkiMIS`kGNtAw=>$be#5bid(~;PO&nw^*Ow0xLSFxc}J?0bd9mPp<=p*MBL;+eDBfV zBL^V#G|#da^7h%b5F_3ri-c?ntzg`2T83avAG#OG`jEmXldDjS2C$Y%TwJ_5DT}M< zg&EZr5>~mI+)g3H%PHnXyPZH8XSOnMJ7WTGr`$|%)x8Tf9wnFCDMH{!mdCxQL$Gkn a4*UZ$5oLhfEH2Cd0000 Date: Sun, 10 Nov 2024 09:54:41 +0900 Subject: [PATCH 26/32] add: diagonals.png to example --- examples/bevy-image.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index 218a169..3e552f8 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -32,8 +32,19 @@ fn main() { ) .unwrap(); + 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) { From b1028ece2c11bd08cf5e803f502fd585e09bfb93 Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 10 Nov 2024 09:55:03 +0900 Subject: [PATCH 27/32] removed: broken square.png --- assets/broken-square.png | Bin 296 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/broken-square.png diff --git a/assets/broken-square.png b/assets/broken-square.png deleted file mode 100644 index dd2a5c0f4d40d922898e97485b5667cfdb810c77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 296 zcmV+@0oVSCP)Px#;z>k7R9J=OS3wd3Aqaz={{PGLl9SsNz~Bk!f-C&W75Y z0UBH3-2%4wdI3yvF@c~`2O!01=K)!fi%5CvMjlul%aQ$CVzTj#$SYysDnS)Ybnh;3 zxpun%?Um_9bax?|Jn->Kh`tk^Txh2x)J3O~ct;DcNr=q&0W0XGXkF0&oo&Hk>pgBF uVSa}p$-2{<+*`TjhrSE43Eg_U{~n&c=rKA7{t{0B0000 Date: Sun, 10 Nov 2024 09:56:20 +0900 Subject: [PATCH 28/32] neighbors moved to mod-block into bin_image module --- src/bin_image/mod.rs | 50 +++++++++++++++++++++++++++++++++++--- src/bin_image/neighbors.rs | 8 ------ 2 files changed, 47 insertions(+), 11 deletions(-) delete mode 100644 src/bin_image/neighbors.rs diff --git a/src/bin_image/mod.rs b/src/bin_image/mod.rs index 8dd736b..90595d3 100644 --- a/src/bin_image/mod.rs +++ b/src/bin_image/mod.rs @@ -1,5 +1,14 @@ use crate::{UVec2, Vec2}; -pub mod neighbors; +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, @@ -94,18 +103,53 @@ impl BinImage { if x < u32::MAX && y < u32::MAX && self.get(UVec2::new(x + 1, y + 1)) { neighbors |= neighbors::NORTHEAST; } - if x > u32::MIN && y > u32::MIN && self.get(UVec2::new(x - 1, y - 1)) { + 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::MAX && self.get(UVec2::new(x - 1, y + 1)) { + 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 { + !matches!( + self.get_neighbors(p), + 255 + | 239 + | 238 + | 234..=236 + | 231 + | 223 + | 221 + | 215 + | 213 + | 212 + | 209 + | 204 + | 201 + | 195..=197 + | 188..=192 + | 186 + | 184 + | 181 + | 180 + | 127 + | 123 + | 119 + | 118 + | 113..=115 + | 58 + | 56 + | 52..=54 + | 48..=50 + | 0 + ) + } + /// Translates a point in positive (x, y) coordinates to a coordinate system centered at (0, 0). /// /// # Arguments diff --git a/src/bin_image/neighbors.rs b/src/bin_image/neighbors.rs deleted file mode 100644 index 311d9cb..0000000 --- a/src/bin_image/neighbors.rs +++ /dev/null @@ -1,8 +0,0 @@ -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; From df77244fc05604334285ce426b7186030a61ee7b Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 10 Nov 2024 22:15:05 +0900 Subject: [PATCH 29/32] add: rayon to dependecies --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 80f3444..088d505 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,13 @@ 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" From cb7a3b8231940f10e0cf323572ce10fa94a0ba0d Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 10 Nov 2024 22:16:46 +0900 Subject: [PATCH 30/32] bin_image/mod.rs moved to src/ and renamed to bin_image.rs --- src/{bin_image/mod.rs => bin_image.rs} | 50 ++++++-------------------- 1 file changed, 11 insertions(+), 39 deletions(-) rename src/{bin_image/mod.rs => bin_image.rs} (82%) diff --git a/src/bin_image/mod.rs b/src/bin_image.rs similarity index 82% rename from src/bin_image/mod.rs rename to src/bin_image.rs index 90595d3..f2db458 100644 --- a/src/bin_image/mod.rs +++ b/src/bin_image.rs @@ -1,4 +1,5 @@ -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; @@ -37,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) @@ -62,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 } @@ -116,38 +119,7 @@ impl BinImage { } pub fn is_corner(&self, p: UVec2) -> bool { - !matches!( - self.get_neighbors(p), - 255 - | 239 - | 238 - | 234..=236 - | 231 - | 223 - | 221 - | 215 - | 213 - | 212 - | 209 - | 204 - | 201 - | 195..=197 - | 188..=192 - | 186 - | 184 - | 181 - | 180 - | 127 - | 123 - | 119 - | 118 - | 113..=115 - | 58 - | 56 - | 52..=54 - | 48..=50 - | 0 - ) + is_corner(self.get_neighbors(p)) } /// Translates a point in positive (x, y) coordinates to a coordinate system centered at (0, 0). @@ -176,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 { From 8d38555bfa9252a8fe70c799fc68653780641232 Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 10 Nov 2024 22:19:13 +0900 Subject: [PATCH 31/32] reworking of edge collecting algorithm finished --- src/lib.rs | 80 +++++---- src/utils.rs | 496 ++++++++++++++++++++++++--------------------------- 2 files changed, 287 insertions(+), 289 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 746f721..6abfdca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,11 @@ 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 utils::{handle_neighbors, in_polygon, is_corner}; +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 @@ -63,19 +64,20 @@ impl Edges { // any that have at least one transparent / zero value neighbor then, while sorting into drawing // order, groups them into sets of connected pixels 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) && is_corner(image.get_neighbors(*p))) + .filter(|p| image.get(*p) && image.is_corner(*p)) .collect(); let objects: Vec<_> = self .collect_objects(&corners) - .into_iter() - .map(|object| object.into_iter().map(|p| p.as_vec2()).collect()) + .into_par_iter() + .map(|object| object.into_par_iter().map(|p| p.as_vec2()).collect()) .collect(); if translate { objects - .into_iter() - .map(|group| self.translate(group)) + .into_par_iter() + .map(|object| self.translate(object)) .collect() } else { objects @@ -91,33 +93,49 @@ impl Edges { while let Some(start) = corners.iter().find(|point| { objects - .iter() - .all(|object| !object.contains(point) && !in_polygon(**point, object)) + .par_iter() + .all(|object| !(object.contains(point) || in_polygon(**point, object))) }) { - let object = self.collect_object(*start); + 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; + } + }; objects.push(object); } objects } - fn collect_object(&self, mut current: UVec2) -> Vec { - let mut group: Vec = Vec::new(); - group.push(current); - loop { - let (last, neighbors) = (*group.last().unwrap(), self.image.get_neighbors(current)); - if last != current && is_corner(neighbors) { - group.push(current); - } - if let Some(point) = handle_neighbors(&mut current, last, neighbors) { - group.push(point); - } - if current == group[0] { - break group; - } - } - } - /// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0). /// /// # Arguments @@ -163,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 d82eabe..c67a4f2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,37 +1,35 @@ use crate::UVec2; use std::cmp::Ordering::{Equal, Greater, Less}; +// 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))) +} + pub fn in_polygon(point: UVec2, polygon: &[UVec2]) -> bool { - let mut is_inside = false; - for win in polygon.windows(2) { - let (p1, p2) = (win[0], win[1]); - if (p1.x.min(p2.x) <= point.x && point.x <= p1.x.max(p2.x)) - && (p1.y.min(p2.y) <= point.y && point.y <= p1.y.max(p2.y)) - && (p1.y.max(p2.y) - p1.y.min(p2.y)) * (point.x - p1.x.min(p2.x)) - == (p2.x.max(p2.x) - p1.x.min(p2.x)) * (point.y - p1.y.min(p2.y)) - { - return true; + 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 is_inside = false; - if p1.y <= point.y && point.y < p2.y || p2.y <= point.y && point.y < p1.y { - let (point_x, offset_x) = point - .x - .checked_sub(p1.x) - .map_or_else(|| (0, p1.x - point.x), |dx| (dx, 0)); - let (point_y, offset_y) = point - .y - .checked_sub(p1.y) - .map_or_else(|| (0, p1.y - point.y), |dy| (dy, 0)); + 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); - let (dx, offset_x) = (p2.x + offset_x) - .checked_sub(p1.x) - .map_or_else(|| (0, p1.x - p2.x - offset_x), |dx| (dx, 0)); - let (dy, offset_y) = (p2.y + offset_y) - .checked_sub(p1.y) - .map_or_else(|| (0, p1.y - p2.y - offset_y), |dy| (dy, 0)); - if (point_x + offset_x) * dy >= dx * (point_y + offset_y) { - is_inside = !is_inside; + 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 @@ -43,293 +41,275 @@ pub fn is_corner(neighbors: u8) -> bool { 255 | 239 | 238 - | 232..=235 - | 127 - | 226 + | 235 + | 234 | 223 | 221 | 215 | 213 - | 212 - | 209 - | 207 - | 201 - | 200 - | 196..=198 - | 191..=194 - | 189 - | 187 - | 185 - | 184 - | 177 - | 126 + | 188..=207 + | 127 + | 123 | 119 - | 118 - | 116 - | 114 - | 58 - | 56 - | 48..=54 + | 115 + | 48..=63 + | 9 + | 6 | 0 ) } +pub enum Direction { + North, + South, + East, + West, + + Northeast, + Northwest, + Southeast, + Southwest, +} + #[allow(clippy::too_many_lines)] -pub fn handle_neighbors(current: &mut UVec2, last: UVec2, neighbors: u8) -> Option { +pub fn handle_neighbors(current: UVec2, last: UVec2, neighbors: u8) -> Direction { + use Direction::{East, North, Northeast, Northwest, South, Southeast, Southwest, West}; match neighbors { - 253 | 169..=171 | 40 | 38 | 32 => current.x += 1, - 254 | 85..=87 | 16 => current.x -= 1, - 251 | 110 | 106 | 102 | 64 => current.y -= 1, - 247 | 153 | 157 | 149 | 129 | 128 => current.y += 1, - 233 | 191 | 189 | 187 | 185 | 127 | 126 | 119 | 118 | 58 | 56 | 48..=54 => { - match last.x.cmp(¤t.x) { - Greater | Equal => current.x -= 1, - Less => current.x += 1, - } - } - 239 | 238 | 235 | 234 | 223 | 221 | 215 | 213 | 207 | 201 | 200 | 196..=198 | 192..=194 => { - match last.y.cmp(¤t.y) { - Greater | Equal => current.y -= 1, - Less => current.y += 1, - } - } - 168 | 162 | 160 => match last.x.cmp(¤t.x) { - Greater => current.y += 1, - Equal => current.x += 1, + 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!(), }, - 145 | 144 => match last.x.cmp(¤t.x) { + 251 | 157 | 156 | 153 | 152 | 149 | 148 | 145 | 144 => match last.x.cmp(¤t.x) { Greater => unreachable!(), - Equal => current.x -= 1, - Less => current.y += 1, + 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, }, - 90 | 84 | 82 | 80 => match last.x.cmp(¤t.x) { + 186 | 184 => match last.x.cmp(¤t.x) { Greater => unreachable!(), - Equal => current.x -= 1, - Less => current.y -= 1, + Equal => West, + Less => East, }, - 98 | 96 => match last.x.cmp(¤t.x) { - Greater => current.y -= 1, - Equal => current.x += 1, + 231 | 226 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => South, Less => unreachable!(), }, - 177 => match last.x.cmp(¤t.x) { - Greater => { - current.y += 1; - return Some(current.with_y(current.y - 1)); - } + 236 | 232 => match last.y.cmp(¤t.y) { + Greater => South, Equal => unreachable!(), - Less => current.x += 1, + Less => East, }, - 184 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => { - current.x -= 1; - return Some(current.with_x(current.x + 1)); + 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!(), } - Less => current.x += 1, + } + + 110 | 103 | 102 => match last.x.cmp(¤t.x) { + Greater => Northwest, + Equal => unreachable!(), + Less => South, }, - 212 => match last.y.cmp(¤t.y) { - Greater => { - current.x -= 1; - return Some(current.with_x(current.x + 1)); - } + 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 => current.y += 1, + Less => East, + }, + 175 | 173 | 171 | 169 | 167 | 165 | 163 | 161 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => Southwest, + Less => East, }, - 209 => match last.y.cmp(¤t.y) { + 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 => { - current.y -= 1; - return Some(current.with_y(current.y + 1)); - } - Less => current.y += 1, + Equal => Southwest, + Less => North, + }, + 94 | 92 | 90 | 88 => match last.x.cmp(¤t.x) { + Greater => West, + Equal => Northeast, + Less => South, }, - 114 => match last.x.cmp(¤t.x) { - Greater => current.x -= 1, + 23 | 22 | 19 | 18 => match last.x.cmp(¤t.x) { + Greater => West, Equal => unreachable!(), - Less => { - current.y -= 1; - return Some(current.with_y(current.y + 1)); - } + Less => Southeast, }, - 116 => match last.x.cmp(¤t.x) { - Greater => current.x -= 1, - Equal => { - current.x += 1; - return Some(current.with_x(current.x - 1)); - } - Less => unreachable!(), + 159 | 158 | 155 | 154 | 151 | 150 | 147 | 146 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => West, + Less => Southeast, }, - 232 => match last.y.cmp(¤t.y) { - Greater => current.y -= 1, + 29 | 28 | 25 | 24 => match last.x.cmp(¤t.x) { + Greater => West, Equal => unreachable!(), - Less => { - current.x += 1; - return Some(current.with_x(current.x - 1)); - } + Less => Northeast, }, - 226 => match last.y.cmp(¤t.y) { - Greater => current.y -= 1, - Equal => { - current.y -= 1; - return Some(current.with_y(current.y + 1)); - } + 72..=75 => match last.x.cmp(¤t.x) { + Greater => South, + Equal => Northeast, Less => unreachable!(), }, - 240 => match (last.x.cmp(¤t.x), last.y.cmp(¤t.y)) { - (Less, Equal) => current.y -= 1, - (Equal, Less) => current.x += 1, - (Greater, Equal) => current.y += 1, - (Equal, Greater) => current.x -= 1, - _ => unreachable!(), - }, - 249 => match last.x.cmp(¤t.x) { + 68..=71 => match last.x.cmp(¤t.x) { Greater => unreachable!(), - Equal => current.x += 1, - Less => current.y -= 1, + Equal => Northwest, + Less => South, }, - 115 | 112 => match last.x.cmp(¤t.x) { - Greater => current.x -= 1, - Equal => current.x += 1, - Less => current.y -= 1, + + 31 | 30 | 27 | 26 => match last.y.cmp(¤t.y) { + Greater => West, + Equal => Southeast, + Less => Northeast, }, - 246 => match last.x.cmp(¤t.x) { - Greater => current.y += 1, - Equal => current.x -= 1, - Less => current.x += 1, + 76..=79 => match last.x.cmp(¤t.x) { + Greater => Northwest, + Equal => Northeast, + Less => South, }, - 186 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => current.x -= 1, - Less => { - current.x += 1; - return Some(current.with_x(current.x - 1)); - } + 47 | 45 | 39 | 37 => match last.y.cmp(¤t.y) { + Greater => Southwest, + Equal => Northwest, + Less => East, }, - 231 => match last.x.cmp(¤t.x) { - Greater => current.y += 1, - Equal => current.y -= 1, - Less => unreachable!(), + 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, }, - 8 => { - current.x += 1; - current.y += 1; - } 12 => match last.x.cmp(¤t.x) { - Greater => { - current.x -= 1; - current.y -= 1; - } + Greater => Northwest, Equal => unreachable!(), - Less => { - current.x += 1; - current.y += 1; - } + Less => Northeast, }, - 44 => match last.x.cmp(¤t.x) { - Greater => { - current.x -= 1; - current.y -= 1; - } + 3 => match last.x.cmp(¤t.x) { + Greater => Southwest, Equal => unreachable!(), - Less => current.x += 1, + Less => Southeast, }, - 24 => match last.x.cmp(¤t.x) { - Greater => current.x -= 1, + 5 => match last.x.cmp(¤t.x) { + Greater => Southwest, Equal => unreachable!(), - Less => { - current.x += 1; - current.y += 1; - } + Less => Northwest, }, - 132 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => { - current.x -= 1; - current.y -= 1; - } - Less => current.y += 1, + 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!(), }, - 103 => match last.x.cmp(¤t.x) { - Greater => { - current.x -= 1; - current.y += 1; - } - Equal => unreachable!(), - Less => current.y -= 1, + + 252 | 124..=126 | 120..=122 | 116..=118 | 112..=114 => match last.x.cmp(¤t.x) { + Greater => West, + Equal => East, + Less => South, }, - 2 => { - current.x += 1; - current.y -= 1; - } - 67 | 65 => match last.x.cmp(¤t.x) { - Greater => unreachable!(), - Equal => { - current.x -= 1; - current.y += 1; - } - Less => current.y -= 1, + 243 | 187 | 185 | 183 | 176..=179 => match last.x.cmp(¤t.x) { + Greater => North, + Equal => West, + Less => East, }, - 3 => match last.x.cmp(¤t.x) { - Greater => { - current.x -= 1; - current.y += 1; - } - Equal => unreachable!(), - Less => { - current.x += 1; - current.y -= 1; - } + 222 | 216..=220 | 214 | 208..=212 => match last.y.cmp(¤t.y) { + Greater => West, + Equal => South, + Less => North, }, - 22 | 18 => match last.x.cmp(¤t.x) { - Greater => current.x -= 1, - Equal => unreachable!(), - Less => { - current.x += 1; - current.y -= 1; - } + 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) => { - current.x -= 1; - current.y += 1; - } - (Less, Greater) => { - current.x -= 1; - current.y -= 1; - } - (Less, Less) => { - current.x += 1; - current.y -= 1; - } + (Greater, Less) => Northwest, + (Less, Less) => Southeast, + (Less, Greater) => Southwest, _ => unreachable!(), }, - 36 => match last.x.cmp(¤t.x) { - Greater => { - current.x -= 1; - current.y -= 1; - } - Equal => unreachable!(), - Less => current.x += 1, + 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!(), }, - 74 => match last.x.cmp(¤t.x) { - Greater => current.y -= 1, - Equal => { - current.x += 1; - current.y += 1; - } - Less => unreachable!(), - } - 250 => match last.y.cmp(¤t.y) { - Greater => current.x -= 1, - Equal => current.y -= 1, - Less => current.y += 1, - } - 0 | 255 => unreachable!(), - _ => { - todo!("\nadd handle for this case:\nlast point:{last}\ncurrent point:{current}\ncurrent neighbors:{neighbors}") - } } - None } From 075d42b4bc15ef34e6b1e8b4b06afc7bbfd0418b Mon Sep 17 00:00:00 2001 From: salam Date: Sun, 10 Nov 2024 22:21:12 +0900 Subject: [PATCH 32/32] replaced pass by value with pass by reference --- examples/bevy-image.rs | 13 +++++++------ examples/dynamic-image.rs | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index 3e552f8..1af434b 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -42,19 +42,20 @@ fn main() { ) .unwrap(); - draw_png(boulders, "boulders"); - draw_png(more_lines, "more-lines"); - draw_png(diagonals, "diagonals"); + 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); 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,