-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathedges.rs
321 lines (277 loc) · 10.1 KB
/
edges.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
use std::fmt;
use glam::Vec2;
pub enum Edges {
DynamicImage(image::DynamicImage),
#[cfg(feature = "bevy")]
BevyImage(bevy::prelude::Image),
}
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)
pub fn single_image_edge_translated(&self) -> Vec<Vec2> {
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
pub fn single_image_edge_raw(&self) -> Vec<Vec2> {
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)
pub fn multi_image_edge_translated(&self) -> Vec<Vec<Vec2>> {
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
pub fn multi_image_edges_raw(&self) -> Vec<Vec<Vec2>> {
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
pub fn image_edges(&self, translate: bool) -> Vec<Vec<Vec2>> {
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<usize> = 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<Vec<Vec2>> {
let mut edge_points: Vec<Vec2> = 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 {
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),
];
let n: usize = neighbors.iter().sum();
let surrounded = neighbors.len();
if n < surrounded {
edge_points.push(Vec2::new(x, y));
}
}
Edges::points_to_drawing_order(&edge_points, translate, rows, cols)
}
/// 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<Vec<Vec2>> {
let mut edge_points: Vec<Vec2> = points.to_vec();
let mut in_drawing_order: Vec<Vec2> = vec![];
let mut groups: Vec<Vec<Vec2>> = vec![];
while !edge_points.is_empty() {
if in_drawing_order.is_empty() {
in_drawing_order.push(edge_points.swap_remove(0));
}
let prev = *in_drawing_order.last().unwrap();
let neighbor = edge_points
.iter()
.enumerate()
.find(|(_, p)| Edges::distance(prev, **p) == 1.0);
if let Some((i, _)) = neighbor {
let next = edge_points.remove(i);
in_drawing_order.push(next);
continue;
}
if !in_drawing_order.is_empty() {
groups.push(in_drawing_order.clone());
in_drawing_order.clear()
}
}
if !in_drawing_order.is_empty() {
groups.push(in_drawing_order.clone());
}
if translate {
groups = groups
.into_iter()
.map(|p| Edges::translate_vec(p, rows, cols))
.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()
}
/// 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
} 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)
}
}
/// translate point in positive x,y to either side of (0,0)
fn xy_translate(p: Vec2, rows: usize, cols: usize) -> Vec2 {
Vec2::new(
p.x - (cols as f32 / 2. - 1.0),
-p.y + (rows as f32 / 2. - 1.0),
)
}
/// Translate vector of points in positive x,y to either side of (0,0)
pub fn translate_vec(v: Vec<Vec2>, rows: usize, cols: usize) -> Vec<Vec2> {
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<bevy::prelude::Image> for Edges {
fn from(i: bevy::prelude::Image) -> Edges {
Edges::BevyImage(i)
}
}
#[cfg(feature = "bevy")]
impl From<&bevy::prelude::Image> for Edges {
fn from(i: &bevy::prelude::Image) -> Edges {
Edges::BevyImage(i.clone())
}
}
impl From<image::DynamicImage> for Edges {
fn from(i: image::DynamicImage) -> Edges {
Edges::DynamicImage(i)
}
}
impl From<&image::DynamicImage> for Edges {
fn from(i: &image::DynamicImage) -> Edges {
Edges::DynamicImage(i.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<Vec<Vec2>>,
translated: Vec<Vec<Vec2>>,
}
let edges_display = EdgesDisplay {
raw: self.image_edges(false),
translated: self.image_edges(false),
};
write!(f, "{:#?}", edges_display)
}
}
#[cfg(feature = "bevy")]
mod tests {
#[allow(unused_imports)]
use super::Edges;
#[allow(unused_imports)]
use bevy::{prelude::Image, render::texture::ImageType};
#[allow(unused_imports)]
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"),
Default::default(),
true, //
Default::default(),
Default::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"),
Default::default(),
true, //
Default::default(),
Default::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()
);
}
}