Skip to content

Commit

Permalink
Merge pull request #1 from salam99823/dev
Browse files Browse the repository at this point in the history
Finally, the edge collection algorithm has been rewritten to work with diagonal lines and objects with holes.
  • Loading branch information
salam99823 authored Nov 10, 2024
2 parents 5301356 + 075d42b commit bd9591f
Show file tree
Hide file tree
Showing 7 changed files with 455 additions and 109 deletions.
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,17 @@ repository = "https://github.com/shnewto/edges"
license = "MIT OR Apache-2.0"

[lints.clippy]
type_complexity = { level = "allow", priority = 1 }
needless_pass_by_value = { level = "allow", priority = 1 }
cast_precision_loss = { level = "allow", priority = 1 }
pedantic = { level = "warn", priority = 0 }

[features]
default = ["glam-latest"]
default = ["bevy"]
glam-latest = ["dep:glam"]
bevy = ["dep:bevy_math", "dep:bevy_render"]

[dependencies]
image = "0.25"
rayon = "1.10.0"

[dependencies.glam]
version = "0.29"
Expand Down
Binary file added assets/diagonals.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 19 additions & 7 deletions examples/bevy-image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,37 @@ fn main() {
)
.unwrap();

draw_png(boulders, "boulders");
draw_png(more_lines, "more-lines");
let diagonals = Image::from_buffer(
include_bytes!("../assets/diagonals.png"),
ImageType::Extension("png"),
CompressedImageFormats::default(),
true,
ImageSampler::default(),
RenderAssetUsages::default(),
)
.unwrap();

draw_png(&boulders, "boulders");
draw_png(&more_lines, "more-lines");
draw_png(&diagonals, "diagonals");
}

fn draw_png(image: Image, img_path: &str) {
fn draw_png(image: &Image, img_path: &str) {
// get the image's edges
let edges = Edges::from(image);

let scale = 8;
let (width, height) = (
i32::try_from(image.width()).expect("Image to wide.") * scale,
i32::try_from(image.height()).expect("Image to tall.") * scale,
);
// get the image's edges
let edges = Edges::from(image);

// draw the edges to a png
let mut dt = DrawTarget::new(width, height);

let objects_iter = edges.multi_image_edges_raw().into_iter();
let objects = edges.multi_image_edges_raw();

for object in objects_iter {
for object in objects {
let mut pb = PathBuilder::new();
let mut edges_iter = object.into_iter();

Expand Down
6 changes: 4 additions & 2 deletions examples/dynamic-image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
75 changes: 54 additions & 21 deletions src/bin_image.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
use crate::{UVec2, Vec2};
use crate::{utils::is_corner, UVec2, Vec2};
use rayon::prelude::*;
pub mod neighbors {
pub const NORTH: u8 = 0b1000_0000;
pub const SOUTH: u8 = 0b0100_0000;
pub const EAST: u8 = 0b0010_0000;
pub const WEST: u8 = 0b0001_0000;
pub const NORTHEAST: u8 = 0b0000_1000;
pub const NORTHWEST: u8 = 0b0000_0100;
pub const SOUTHEAST: u8 = 0b0000_0010;
pub const SOUTHWEST: u8 = 0b0000_0001;
}

pub struct BinImage {
data: Vec<u8>,
Expand Down Expand Up @@ -27,10 +38,10 @@ impl BinImage {
let compress_step = data.len() / (height * width) as usize;
Self {
data: data
.chunks(8 * compress_step)
.par_chunks(8 * compress_step)
.map(|chunk| {
chunk
.chunks(compress_step)
.par_chunks(compress_step)
.map(|chunk| chunk.iter().any(|i| *i != 0))
.enumerate()
.map(|(index, bit)| u8::from(bit) << index)
Expand All @@ -52,15 +63,17 @@ impl BinImage {
///
/// Returns `true` if the pixel is "on" (1), and `false` if it is "off" (0) or out of bounds.
pub fn get(&self, p: UVec2) -> bool {
let (x, y) = (p.x, p.y);
let index = y * self.width + x;
if p.x >= self.width {
return false;
}
let index = p.y * self.width + p.x;
if let Some(mut byte) = self
.data
.get((index / 8) as usize) // index of byte
.copied()
{
byte >>= index % 8; // index of bit
x <= self.width && byte & 1 > 0
byte & 1 > 0
} else {
false
}
Expand All @@ -74,19 +87,39 @@ impl BinImage {
///
/// # Returns
///
/// An array of 8 boolean values representing the state of the neighboring pixels.
pub fn get_neighbors(&self, p: UVec2) -> [bool; 8] {
/// An byte representing the state of the neighboring pixels.
pub fn get_neighbors(&self, p: UVec2) -> u8 {
let (x, y) = (p.x, p.y);
[
y < u32::MAX && self.get((x, y + 1).into()), // North
y > u32::MIN && self.get((x, y - 1).into()), // South
x < u32::MAX && self.get((x + 1, y).into()), // East
x > u32::MIN && self.get((x - 1, y).into()), // West
x < u32::MAX && y < u32::MAX && self.get((x + 1, y + 1).into()), // Northeast
x > u32::MIN && y > u32::MIN && self.get((x - 1, y - 1).into()), // Southwest
x < u32::MAX && y > u32::MIN && self.get((x + 1, y - 1).into()), // Southeast
x > u32::MIN && y < u32::MAX && self.get((x - 1, y + 1).into()), // Northwest
]
let mut neighbors = 0;
if y < u32::MAX && self.get(UVec2::new(x, y + 1)) {
neighbors |= neighbors::NORTH;
}
if y > u32::MIN && self.get(UVec2::new(x, y - 1)) {
neighbors |= neighbors::SOUTH;
}
if x < u32::MAX && self.get(UVec2::new(x + 1, y)) {
neighbors |= neighbors::EAST;
}
if x > u32::MIN && self.get(UVec2::new(x - 1, y)) {
neighbors |= neighbors::WEST;
}
if x < u32::MAX && y < u32::MAX && self.get(UVec2::new(x + 1, y + 1)) {
neighbors |= neighbors::NORTHEAST;
}
if x > u32::MIN && y < u32::MAX && self.get(UVec2::new(x - 1, y + 1)) {
neighbors |= neighbors::NORTHWEST;
}
if x < u32::MAX && y > u32::MIN && self.get(UVec2::new(x + 1, y - 1)) {
neighbors |= neighbors::SOUTHEAST;
}
if x > u32::MIN && y > u32::MIN && self.get(UVec2::new(x - 1, y - 1)) {
neighbors |= neighbors::SOUTHWEST;
}
neighbors
}

pub fn is_corner(&self, p: UVec2) -> bool {
is_corner(self.get_neighbors(p))
}

/// Translates a point in positive (x, y) coordinates to a coordinate system centered at (0, 0).
Expand All @@ -100,8 +133,8 @@ impl BinImage {
/// A new `Vec2` representing the translated coordinates
fn translate_point(&self, p: Vec2) -> Vec2 {
Vec2::new(
p.x - (self.width as f32 / 2.0 - 1.0),
(self.height as f32 / 2.0 - 1.0) - p.y,
p.x - ((self.width / 2) as f32 - 1.0),
((self.height / 2) as f32 - 1.0) - p.y,
)
}

Expand All @@ -115,7 +148,7 @@ impl BinImage {
///
/// A vector of `Vec2` representing the translated coordinates.
pub fn translate(&self, v: Vec<Vec2>) -> Vec<Vec2> {
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 {
Expand Down
98 changes: 76 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
#![doc = include_str!("../README.md")]

use crate::bin_image::BinImage;
#[cfg(feature = "bevy")]
pub use bevy_math::prelude::{UVec2, Vec2};
#[cfg(not(feature = "bevy"))]
#[cfg(all(not(feature = "bevy"), feature = "glam-latest"))]
pub use glam::{UVec2, Vec2};
use rayon::prelude::*;
use std::fmt;

use crate::{bin_image::BinImage, utils::points_to_drawing_order};
use utils::{handle_neighbors, in_polygon, Direction};

mod bin_image;
#[cfg(feature = "bevy")]
Expand All @@ -30,14 +31,14 @@ impl Edges {
/// coordinates translated to either side of (0, 0)
#[must_use]
pub fn single_image_edge_translated(&self) -> Vec<Vec2> {
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<Vec2> {
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
Expand All @@ -62,24 +63,77 @@ impl Edges {
// Marching squares adjacent, walks all the pixels in the provided data and keeps track of
// any that have at least one transparent / zero value neighbor then, while sorting into drawing
// order, groups them into sets of connected pixels
let edge_points = (0..image.height() * image.width())
.map(|i| (i / image.height(), i % image.height()))
.map(|(x, y)| UVec2::new(x, y))
.filter(|p| image.get(*p))
.filter(|p| (0..8).contains(&image.get_neighbors(*p).iter().filter(|i| **i).count()))
let corners: Vec<_> = (0..image.height() * image.width())
.into_par_iter()
.map(|i| UVec2::new(i / image.height(), i % image.height()))
.filter(|p| image.get(*p) && image.is_corner(*p))
.collect();

let objects: Vec<_> = self
.collect_objects(&corners)
.into_par_iter()
.map(|object| object.into_par_iter().map(|p| p.as_vec2()).collect())
.collect();
if translate {
objects
.into_par_iter()
.map(|object| self.translate(object))
.collect()
} else {
objects
}
}

points_to_drawing_order(edge_points)
.into_iter()
.map(|group| {
let group = group.into_iter().map(|p| p.as_vec2()).collect();
if translate {
self.translate(group)
} else {
group
fn collect_objects(&self, corners: &[UVec2]) -> Vec<Vec<UVec2>> {
if corners.is_empty() {
return Vec::new();
}

let mut objects: Vec<Vec<UVec2>> = Vec::new();

while let Some(start) = corners.iter().find(|point| {
objects
.par_iter()
.all(|object| !(object.contains(point) || in_polygon(**point, object)))
}) {
let mut current = *start;
let mut group: Vec<UVec2> = Vec::new();
group.push(current);
let object = loop {
let (last, neighbors) = (*group.last().unwrap(), self.image.get_neighbors(current));
if last != current {
group.push(current);
}
match handle_neighbors(current, last, neighbors) {
Direction::North => current.y += 1,
Direction::South => current.y -= 1,
Direction::East => current.x += 1,
Direction::West => current.x -= 1,
Direction::Northeast => {
current.x += 1;
current.y += 1;
}
Direction::Northwest => {
current.x -= 1;
current.y += 1;
}
Direction::Southeast => {
current.x += 1;
current.y -= 1;
}
Direction::Southwest => {
current.x -= 1;
current.y -= 1;
}
}
if current == *start {
break group;
}
})
.collect()
};
objects.push(object);
}

objects
}

/// Translates an `Vec` of points in positive (x, y) coordinates to a coordinate system centered at (0, 0).
Expand Down Expand Up @@ -127,9 +181,9 @@ impl fmt::Debug for Edges {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
"Edges {{{}\n}}",
format!(
"Edges {{\nraw: {:#?},\ntranslated: {:#?}\n}}",
"\nraw: {:#?},\ntranslated: {:#?}",
self.image_edges(false),
self.image_edges(true),
)
Expand Down
Loading

0 comments on commit bd9591f

Please sign in to comment.