From f4f103239990d887db86c3fe4a1b1bed6c6bcc45 Mon Sep 17 00:00:00 2001 From: shnewto Date: Sat, 11 May 2024 11:48:35 -0400 Subject: [PATCH] Points to drawing order wasn't backtracking The previous approach to collecting points to drawing order didn't account correctly for 'dead ends', i.e. pixels without a neighbor like the antenna in the car sprite included in assets directory. The code had what turned out to be a fragile workaround that really only worked for the car sprite: jumbling the order of the initial points a bit. This commit gets rid of the workaround, and handles backtracking by walking backward when we are at a pixel with no neighbor but we're not out of points. --- .gitignore | 1 + Cargo.toml | 7 ++++ assets/lines.png | Bin 0 -> 191 bytes assets/more-lines.png | Bin 0 -> 530 bytes assets/terrain.png | Bin 0 -> 1491 bytes examples/bevy-image.rs | 66 ++++++++++++++++++++++++++++++++---- examples/dynamic-image.rs | 44 ++++++++++++++++++++++-- src/edges.rs | 68 +++++++++++++++++++++++++++----------- 8 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 assets/lines.png create mode 100644 assets/more-lines.png create mode 100644 assets/terrain.png 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 0000000000000000000000000000000000000000..01baa3271567a699a88b3f6ee0df5a649384abd3 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}m7Xq+AsLNt zuRHQKIPkDu+|Q!zabQ1#Nzopr0MlbY;{X5v literal 0 HcmV?d00001 diff --git a/assets/more-lines.png b/assets/more-lines.png new file mode 100644 index 0000000000000000000000000000000000000000..5e48aed287116498e5f44e3518c183b2fd04e110 GIT binary patch literal 530 zcmeAS@N?(olHy`uVBq!ia0vp^DIm~jwHU~QB-{{H6UihZfAUb9x#PcqXveemwN>(%d* zH40zVGE}7rNHiaiU^%8>z}9HU(c_rl)Z9}h==OvI!sB7u{LQZ6qHBK2zN)<+SHC}f z{#mkSp>_QG$otagbMtMlqiY9hc*(WxE9?C&;nyzi30nVr3TyHNg}}|tj&?6h|4OkO zvkwsck$)veGSs(xg7Vu6uMy=COz^*D;*1?A3l5MW6=`vMuz}yt%hE; zz1(p-PEle>~xujk9_?vEGQ2{s?t6YK2N7I9>Y>8V{!&!vR^S1n&7 zYvjJgF#+UMVCXJkDU`PE=*fK_xa|_ZUnD4?JCb&`X-tKyfQJ81)=jSuR{J~lNdY67 N!PC{xWt~$(699{O-CqCz literal 0 HcmV?d00001 diff --git a/assets/terrain.png b/assets/terrain.png new file mode 100644 index 0000000000000000000000000000000000000000..98dfa7ef22fe3d16a88474b04174bc0d2a0650b0 GIT binary patch literal 1491 zcmd6n{XY|U0LQ2x=1olSvgNN6?Yzn3d_T^PBe_X z;#Kn8T{g<5LY&90RpcQn)I7~oo_G2$?(@U@^ZxDg+b5mu;i96nM+pF+LUJXZ06=bA z0Rthw&3LaqZ!H2{tX_*K}kySvs+oRlLDVBAj_JMlh9vCSXKB=(XtHa&IE(wk^NY>xsjsMl?!2 zLR#6Y@zd@U&F|CyuLw5G%4PuJ;f!BmN!YqIcVjCxKL5D)c>uX=yta;zQ|^+3P8y%= zb0DJ8Hb|x76ia5p-T(&5Y#(E1nc0Rr3_4Kb4MJb|PH_-Wn_2=RtcxeOx?el0+X>vd z6I2rEjBRJLRSk}Pob|xEay6692Cx+t>Si6NZU3& z4V8>v`X;oK1#@mV92U;S!;(Jcd7}~6i!2r8H}eUj=<9_%l+u9ZY4!E_H#A$FNFG)s z!nfds7EcdY(pke`@NKw(|Deo?N>%IaSNh9J2;CE;Wk~M=0akv zUBZOjk79e-rQgOKM`xBMOiK%v$P9TqcygHhp8hA1RP&pG$0<=a7omb2@gRO&^t>TO zP;Ptco>I37fbALi^1RuQO6NX}uAJIvC0XolKfz2w z^B_yo%|_4gq5kUTyINo^dv>ueCSY{oPIdV8LlA(VWkE1sRb_Kq?9R%oRLBK-Cx6Xe zf}GZp1YVJ@EG%2A0ria8?#RTyeM+fK^I`j6sLWA~#0jr}dfqYZw1;em;ii*%W@yat z<5S_)SQukkO#As6Wm=3q+Vc@9s} zAMr>WsN4ks+Jm3j18;F?etTlXG|GM$&NF}AJucc&1Jul8!&JqUi%@4pt{%}`A8C5<6SYKN0LJ5e0m_RQ{d@%nCgzqg6 zbZDi+lz-nboQBi?LiWCj;zC&sr8C-7aTJW2Q=h`5JDB`Lz*Hx6=zh#2Gm2php958* zY^+D$Geye~Lq&hwZWtpr&i9eelfZr8XpSpzofQYx^MhE6qhL()G)S8(oXgo+{$AnQ u`^(zs15h{vfsEBr)GWuQqRy3r`9+6f!+fG&ZplC2Pm^@SgIML{pZp)NfM`Mh literal 0 HcmV?d00001 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..59eb37e 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)); +} \ No newline at end of file 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