diff --git a/.gitignore b/.gitignore index 196e176..1c894f6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +edges-*.png # Added by cargo diff --git a/Cargo.toml b/Cargo.toml index 75ddc21..49f9354 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,10 @@ bevy=["dep:bevy"] [dependencies] glam = "0.25.0" +hashbrown = "0.14.5" image = "0.24.9" +mashmap = "0.1.1" +ordered-float = "4.2.0" thiserror = "1.0.57" [dependencies.bevy] @@ -29,6 +32,10 @@ default-features = false features = ["bevy_render"] optional = true +[dev-dependencies] +raqote = "0.8.4" +open = "5.1.2" + [[example]] name = "bevy-image" diff --git a/assets/lines.png b/assets/lines.png new file mode 100644 index 0000000..01baa32 Binary files /dev/null and b/assets/lines.png differ diff --git a/assets/more-lines.png b/assets/more-lines.png new file mode 100644 index 0000000..5e48aed Binary files /dev/null and b/assets/more-lines.png differ diff --git a/assets/terrain.png b/assets/terrain.png new file mode 100644 index 0000000..98dfa7e Binary files /dev/null and b/assets/terrain.png differ diff --git a/examples/bevy-image.rs b/examples/bevy-image.rs index fde09fa..7dd6852 100644 --- a/examples/bevy-image.rs +++ b/examples/bevy-image.rs @@ -1,22 +1,76 @@ use bevy::{prelude::Image, render::texture::ImageType}; use edges::Edges; - +use raqote::*; // 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();` // let e = Edges::from(image); fn main() { // read png as bytes and manually construct a bevy Image - let image = Image::from_buffer( - include_bytes!("../assets/car.png"), + + let boulders = Image::from_buffer( + include_bytes!("../assets/boulders.png"), + ImageType::Extension("png"), + Default::default(), + true, + Default::default(), + Default::default(), + ) + .unwrap(); + + let more_lines = Image::from_buffer( + include_bytes!("../assets/more-lines.png"), ImageType::Extension("png"), Default::default(), true, Default::default(), Default::default(), - ); + ) + .unwrap(); + draw_png(boulders, "boulders.png"); + draw_png(more_lines, "more-lines.png"); +} + +fn draw_png(image: Image, img_path: &str) { // get the image's edges - let edges = Edges::from(image.unwrap()); - println!("{:#?}", edges.single_image_edge_translated()); + let edges = Edges::from(image.clone()); + let scale = 8; + let (width, height) = (image.width() as i32 * scale, image.height() as i32 * scale); + + // draw the edges to a png + let mut dt = DrawTarget::new(width, height); + + let objects_iter = edges.multi_image_edges_raw().into_iter(); + + for object in objects_iter { + let mut pb = PathBuilder::new(); + let mut edges_iter = object.into_iter(); + + if let Some(first_edge) = edges_iter.next() { + pb.move_to(first_edge.x * scale as f32, first_edge.y * scale as f32); + for edge in edges_iter { + pb.line_to(edge.x * scale as f32, edge.y * scale as f32); + } + } + + let path = pb.finish(); + dt.stroke( + &path, + &Source::Solid(SolidSource { + r: 0xff, + g: 0xff, + b: 0xff, + a: 0xff, + }), + &StrokeStyle { + width: 1., + ..StrokeStyle::default() + }, + &DrawOptions::new(), + ); + } + + dt.write_png(format!("edges-{}", img_path)).unwrap(); + _ = open::that(format!("edges-{}", img_path)); } diff --git a/examples/dynamic-image.rs b/examples/dynamic-image.rs index 3386f85..5c1cb1d 100644 --- a/examples/dynamic-image.rs +++ b/examples/dynamic-image.rs @@ -1,8 +1,46 @@ use edges::Edges; +use raqote::*; use std::path::Path; fn main() { - let image = image::open(Path::new("assets/car.png")); - let edges = Edges::from(image.unwrap()); - println!("{:#?}", edges.single_image_edge_translated()); + draw_png("car.png"); + draw_png("lines.png"); + draw_png("terrain.png"); +} + +fn draw_png(img_path: &str) { + 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); + + // draw the edges to a png + let mut dt = DrawTarget::new(width, height); + let mut pb = PathBuilder::new(); + + let mut edges_iter = edges.single_image_edge_raw().into_iter(); + let first_edge = edges_iter.next().unwrap(); + pb.move_to(first_edge.x * scale as f32, first_edge.y * scale as f32); + for edge in edges_iter { + pb.line_to(edge.x * scale as f32, edge.y * scale as f32); + } + + let path = pb.finish(); + dt.stroke( + &path, + &Source::Solid(SolidSource { + r: 0xff, + g: 0xff, + b: 0xff, + a: 0xff, + }), + &StrokeStyle { + width: 1., + ..StrokeStyle::default() + }, + &DrawOptions::new(), + ); + + dt.write_png(format!("edges-{}", img_path)).unwrap(); + _ = open::that(format!("edges-{}", img_path)); } diff --git a/src/edges.rs b/src/edges.rs index 8f36d01..b64c674 100644 --- a/src/edges.rs +++ b/src/edges.rs @@ -1,6 +1,9 @@ use std::fmt; use glam::Vec2; +use hashbrown::HashSet; +use mashmap::MashMap; +use ordered_float::OrderedFloat; pub enum Edges { DynamicImage(image::DynamicImage), @@ -109,36 +112,63 @@ impl Edges { rows: usize, cols: usize, ) -> Vec> { - let mut edge_points: Vec = points.to_vec(); - let mut in_drawing_order: Vec = vec![]; let mut groups: Vec> = vec![]; - while !edge_points.is_empty() { - if in_drawing_order.is_empty() { - in_drawing_order.push(edge_points.swap_remove(0)); - } + let mut in_drawing_order: Vec = vec![]; + 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; + } - let prev = *in_drawing_order.last().unwrap(); + let mut current = points[0]; + let mut start = current; + in_drawing_order.push(current); + drawn_points_with_counts.insert(hashable(current), ()); + drawn_points.insert(hashable(current)); - let neighbor = edge_points + while drawn_points.len() < points.len() { + let neighbors = &points .iter() - .enumerate() - .find(|(_, p)| Edges::distance(prev, **p) == 1.0); + .filter(|p| Edges::distance(current, **p) == 1.0) + .collect::>(); - if let Some((i, _)) = neighbor { - let next = edge_points.remove(i); - in_drawing_order.push(next); - continue; + if let Some(p) = neighbors + .iter() + .min_by_key(|n| drawn_points_with_counts.get_iter(&hashable(***n)).count()) + { + current = **p; + in_drawing_order.push(**p); + drawn_points_with_counts.insert(hashable(**p), ()); + drawn_points.insert(hashable(**p)); } - if !in_drawing_order.is_empty() { + // 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() + in_drawing_order.clear(); + drawn_points_with_counts.clear(); + + if let Some(c) = points + .iter() + .find(|p| !drawn_points.contains(&hashable(**p))) + { + in_drawing_order.push(*c); + drawn_points_with_counts.insert(hashable(*c), ()); + drawn_points.insert(hashable(*c)); + current = *c; + start = current; + } else { + break; + } } } - if !in_drawing_order.is_empty() { - groups.push(in_drawing_order.clone()); - } + groups.push(in_drawing_order.clone()); if translate { groups = groups