From c78c0bcfee3daafdb929a6fd0ae331146925fd2e Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Tue, 4 Dec 2018 17:28:53 +0100 Subject: [PATCH 01/13] Add data structures and stub methods --- sheep/src/pack/maxrects.rs | 176 +++++++++++++++++++++++++++++++++++++ sheep/src/pack/mod.rs | 1 + 2 files changed, 177 insertions(+) create mode 100644 sheep/src/pack/maxrects.rs diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs new file mode 100644 index 0000000..e3d6419 --- /dev/null +++ b/sheep/src/pack/maxrects.rs @@ -0,0 +1,176 @@ +use {Packer, PackerResult, SpriteAnchor, SpriteData}; + +pub struct MaxrectsPacker; + +impl Packer for MaxrectsPacker { + fn pack(sprites: &[SpriteData]) -> PackerResult { + PackerResult { + dimensions: (0, 0), + anchors: Vec::new(), + } + } +} + +static DEFAULT_BIN_SIZE: u32 = 4096; +static ALLOW_FLIP: bool = false; + +#[derive(Debug, Clone, Copy)] +struct Rect { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +impl Rect { + pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self { + Rect { + x, + y, + width, + height, + } + } + + pub fn contains(&self, other: &Rect) -> bool { + self.x >= other.x + && self.y >= other.y + && self.x + self.width <= other.x + other.width + && self.y + self.height <= other.y + other.height + } +} + +#[derive(Debug, Clone, Copy)] +enum Strategy { + BestShortSideFit, + BestLongSideFit, + BestAreaFit, + BottomLeftRule, + ContactPointRule, +} + +#[derive(Debug, Clone, Copy)] +struct RectScore { + placement: Rect, + primary: i32, + secondary: i32, +} + +#[derive(Debug, Clone)] +struct MaxRectsBins { + bin_width: u32, + bin_height: u32, + allow_flip: bool, + used: Vec<(Rect, usize)>, + free: Vec, +} + +impl MaxRectsBins { + pub fn new() -> Self { + MaxRectsBins { + bin_width: DEFAULT_BIN_SIZE, + bin_height: DEFAULT_BIN_SIZE, + allow_flip: ALLOW_FLIP, + used: Vec::new(), + free: vec![Rect::new(0, 0, DEFAULT_BIN_SIZE, DEFAULT_BIN_SIZE)], + } + } + + pub fn insert_sprites(&mut self, sprites: &[SpriteData]) { + let mut sprites = sprites.iter().cloned().collect::>(); + + while !sprites.is_empty() { + // Score all rects and sort them by their score, best score first + let sorted = sprites + .iter() + .map(|it| (self.score_rect(it.dimensions.0, it.dimensions.1), *it)) + .collect::>(); + + let next = { + // Find out if there's multiple with the best score + let best_scored = sorted + .iter() + .filter(|(score, _)| score.primary == sorted[0].0.primary) + .collect::>(); + + // If not, we have found the next best fit! Othweise, take the + // next best by the secondary score + if best_scored.len() == 1 { + best_scored[0] + } else { + best_scored + .iter() + .min_by_key(|(score, _)| score.secondary) + .expect("Unreachable") + } + }; + + self.place_rect(&next.0.placement, &next.1); + sprites.retain(|it| it.id != next.1.id); + } + } + + fn score_rect(&self, width: u32, height: u32) -> RectScore { + use std::cmp::{min, max}; + + // For now, we always use bssf since it is the most efficient by itself + // TODO: Other strategies + global best choice + let mut best_short = std::u32::MAX; + let mut best_long = std::u32::MAX; + let mut placement = Rect::new(0, 0, 0, 0); + + self.free.iter().for_each(|it| { + let leftover_horiz = (it.width as i32 - width as i32).abs() as u32; + let leftover_vert = (it.height as i32 - height as i32).abs() as u32; + + let short_side_fit = min(leftover_horiz, leftover_vert); + let long_side_fit = max(leftover_horiz, leftover_vert); + + if short_side_fit < best_short || + (short_side_fit == best_short && long_side_fit < best_long) { + best_short = short_side_fit; + best_long = long_side_fit; + placement = Rect::new(it.x, it.y, width, height); + } + }); + + RectScore { + placement, + primary: best_short as i32, + secondary: best_long as i32, + } + } + + fn find_position() {} + + fn place_rect(&mut self, rect: &Rect, sprite: &SpriteData) { + + } + + fn prune_free_list(&mut self) { + // TODO(happenslol): This is really ugly, since I haven't found + // a way to either modify the array while iterating over it, or + // modify the iterator variable while going over a range. + // I'm 99% sure there's a better way to do this. + let mut i = 0; + 'outer: while i < self.free.len() { + let mut j = i + 1; + + 'inner: while j < self.free.len() { + if self.free[j].contains(&self.free[i]) { + self.free.remove(i); + i -= 1; + break 'inner; + } + + if self.free[i].contains(&self.free[j]) { + self.free.remove(j); + } else { + j += 1; + } + } + + i += 1; + } + } +} diff --git a/sheep/src/pack/mod.rs b/sheep/src/pack/mod.rs index 74146c4..d6b2d49 100644 --- a/sheep/src/pack/mod.rs +++ b/sheep/src/pack/mod.rs @@ -1,3 +1,4 @@ +pub mod maxrects; pub mod simple; use {SpriteAnchor, SpriteData}; From ac15f144064d60882a6a03a7e190bd34f0a510b7 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Tue, 25 Jun 2019 02:12:11 +0200 Subject: [PATCH 02/13] Add packer options, update return type for multiple sheets --- sheep/examples/simple_pack/main.rs | 12 +++++-- sheep/src/lib.rs | 54 +++++++++++++++++------------- sheep/src/pack/maxrects.rs | 10 ++++-- sheep/src/pack/mod.rs | 4 ++- sheep/src/pack/simple.rs | 16 +++++---- sheep_cli/src/main.rs | 8 ++++- 6 files changed, 68 insertions(+), 36 deletions(-) diff --git a/sheep/examples/simple_pack/main.rs b/sheep/examples/simple_pack/main.rs index 72f8037..0f3efa8 100644 --- a/sheep/examples/simple_pack/main.rs +++ b/sheep/examples/simple_pack/main.rs @@ -27,11 +27,19 @@ fn main() { // Do the actual packing! 4 defines the stride, since we're using rgba8 we // have 4 bytes per pixel. - let sprite_sheet = sheep::pack::(sprites, 4); + let results = sheep::pack::(sprites, 4, ()); + + // SimplePacker always returns a single result. Other packers can return + // multiple sheets; should they, for example, choose to enforce a maximum + // texture size per sheet. + let sprite_sheet = results + .into_iter() + .next() + .expect("Should have returned a spritesheet"); // Now, we can encode the sprite sheet in a format of our choosing to // save things such as offsets, positions of the sprites and so on. - let meta = sheep::encode::(&sprite_sheet); + let meta = sheep::encode::(&sprite_sheet, ()); // Next, we save the output to a file using the image crate again. let outbuf = image::RgbaImage::from_vec( diff --git a/sheep/src/lib.rs b/sheep/src/lib.rs index 92f6d82..8f6ca1d 100644 --- a/sheep/src/lib.rs +++ b/sheep/src/lib.rs @@ -29,7 +29,11 @@ pub struct SpriteSheet { anchors: Vec, } -pub fn pack(input: Vec, stride: usize) -> SpriteSheet { +pub fn pack( + input: Vec, + stride: usize, + options: P::Options, +) -> Vec { let sprites = input .into_iter() .enumerate() @@ -41,29 +45,33 @@ pub fn pack(input: Vec, stride: usize) -> SpriteSheet { .map(|it| it.data) .collect::>(); - let packer_result = P::pack(&sprite_data); - let mut buffer = create_pixel_buffer(packer_result.dimensions, stride); - sprites.into_iter().for_each(|sprite| { - let anchor = packer_result - .anchors - .iter() - .find(|it| it.id == sprite.data.id) - .expect("Should have found anchor for sprite"); - write_sprite( - &mut buffer, - packer_result.dimensions, - stride, - &sprite, - &anchor, - ); - }); + let packer_result = P::pack(&sprite_data, options); - SpriteSheet { - bytes: buffer, - stride: stride, - dimensions: packer_result.dimensions, - anchors: packer_result.anchors, - } + packer_result + .into_iter() + .map(|sheet| { + let mut buffer = create_pixel_buffer(sheet.dimensions, stride); + sprites + .iter() + .filter_map(|sprite| { + sheet + .anchors + .iter() + .find(|anchor| anchor.id == sprite.data.id) + .map(|anchor| (sprite, anchor)) + }) + .for_each(|(sprite, anchor)| { + write_sprite(&mut buffer, sheet.dimensions, stride, &sprite, &anchor); + }); + + SpriteSheet { + bytes: buffer, + stride: stride, + dimensions: sheet.dimensions, + anchors: sheet.anchors, + } + }) + .collect() } pub fn encode(sprite_sheet: &SpriteSheet, options: F::Options) -> F::Data diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index e3d6419..aee4735 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -2,12 +2,16 @@ use {Packer, PackerResult, SpriteAnchor, SpriteData}; pub struct MaxrectsPacker; +pub struct MaxrectsOptions; + impl Packer for MaxrectsPacker { - fn pack(sprites: &[SpriteData]) -> PackerResult { - PackerResult { + type Options = MaxrectsOptions; + + fn pack(sprites: &[SpriteData], options: MaxrectsOptions) -> Vec { + vec![PackerResult { dimensions: (0, 0), anchors: Vec::new(), - } + }] } } diff --git a/sheep/src/pack/mod.rs b/sheep/src/pack/mod.rs index d6b2d49..9a29177 100644 --- a/sheep/src/pack/mod.rs +++ b/sheep/src/pack/mod.rs @@ -10,5 +10,7 @@ pub struct PackerResult { } pub trait Packer { - fn pack(sprites: &[SpriteData]) -> PackerResult; + type Options; + + fn pack(sprites: &[SpriteData], options: Self::Options) -> Vec; } diff --git a/sheep/src/pack/simple.rs b/sheep/src/pack/simple.rs index 16ce270..32926f1 100644 --- a/sheep/src/pack/simple.rs +++ b/sheep/src/pack/simple.rs @@ -4,7 +4,9 @@ use {Packer, PackerResult, SpriteAnchor, SpriteData}; pub struct SimplePacker; impl Packer for SimplePacker { - fn pack(sprites: &[SpriteData]) -> PackerResult { + type Options = (); + + fn pack(sprites: &[SpriteData], _options: ()) -> Vec { let mut sprites = sprites.iter().cloned().collect::>(); let mut free = Vec::new(); @@ -74,10 +76,12 @@ impl Packer for SimplePacker { // input sprites absolute.sort_by_key(|s| s.id); - PackerResult { + let result = PackerResult { dimensions: (width, height), anchors: absolute, - } + }; + + vec![result] } } @@ -105,9 +109,9 @@ mod tests { .map(|i| SpriteData::new(i, (20, 20))) .collect::>(); - let result = SimplePacker::pack(&sprites); + let result = SimplePacker::pack(&sprites, ()); - assert_eq!(result.dimensions.0, 20 * 4); - assert_eq!(result.dimensions.1, 20 * 4); + assert_eq!(result[0].dimensions.0, 20 * 4); + assert_eq!(result[0].dimensions.1, 20 * 4); } } diff --git a/sheep_cli/src/main.rs b/sheep_cli/src/main.rs index 80fd852..01bcbaf 100644 --- a/sheep_cli/src/main.rs +++ b/sheep_cli/src/main.rs @@ -122,7 +122,13 @@ where // NOTE(happenslol): By default, we're using rgba8 right now, // so the stride is always 4 - let sprite_sheet = sheep::pack::(sprites, 4); + let results = sheep::pack::(sprites, 4, ()); + + // NOTE(happenslol): SimplePacker always outputs 1 sheet. + let sprite_sheet = results + .into_iter() + .next() + .expect("Should have returned a spritesheet"); let meta = sheep::encode::(&sprite_sheet, options); From 7dd36a374a6435ce27b843c3ee06bae3fc130b75 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Wed, 26 Jun 2019 01:14:14 +0200 Subject: [PATCH 03/13] Implement scoring, placement and splitting --- sheep/src/pack/maxrects.rs | 175 +++++++++++++++++++++++++++++-------- 1 file changed, 137 insertions(+), 38 deletions(-) diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index aee4735..d162be2 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -2,12 +2,33 @@ use {Packer, PackerResult, SpriteAnchor, SpriteData}; pub struct MaxrectsPacker; -pub struct MaxrectsOptions; +pub struct MaxrectsOptions { + preferred_width: u32, + preferred_height: u32, +} impl Packer for MaxrectsPacker { type Options = MaxrectsOptions; fn pack(sprites: &[SpriteData], options: MaxrectsOptions) -> Vec { + let mut bins = vec![MaxRectsBin::new( + options.preferred_width, + options.preferred_height, + )]; + + let mut oversized = Vec::new(); + + for (i, sprite) in sprites.iter().enumerate() { + if sprite.dimensions.0 > options.preferred_width + || sprite.dimensions.1 > options.preferred_height + { + oversized.push(MaxRectsBin::oversized(sprite.dimensions, i)); + continue; + } + + for bin in bins.iter() {} + } + vec![PackerResult { dimensions: (0, 0), anchors: Vec::new(), @@ -15,10 +36,7 @@ impl Packer for MaxrectsPacker { } } -static DEFAULT_BIN_SIZE: u32 = 4096; -static ALLOW_FLIP: bool = false; - -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] struct Rect { pub x: u32, pub y: u32, @@ -42,18 +60,22 @@ impl Rect { && self.x + self.width <= other.x + other.width && self.y + self.height <= other.y + other.height } + + pub fn no_intersection(&self, other: &Rect) -> bool { + self.x >= other.x + other.width + || self.x + self.width <= other.x + || self.y >= other.y + other.height + || self.y + self.height <= other.y + } } -#[derive(Debug, Clone, Copy)] -enum Strategy { - BestShortSideFit, - BestLongSideFit, - BestAreaFit, - BottomLeftRule, - ContactPointRule, +#[derive(Debug, Clone, Copy, PartialEq)] +enum ScoreResult { + NoFit, + FitFound(RectScore), } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] struct RectScore { placement: Rect, primary: i32, @@ -61,22 +83,34 @@ struct RectScore { } #[derive(Debug, Clone)] -struct MaxRectsBins { +struct MaxRectsBin { bin_width: u32, bin_height: u32, - allow_flip: bool, used: Vec<(Rect, usize)>, free: Vec, + oversized: bool, } -impl MaxRectsBins { - pub fn new() -> Self { - MaxRectsBins { - bin_width: DEFAULT_BIN_SIZE, - bin_height: DEFAULT_BIN_SIZE, - allow_flip: ALLOW_FLIP, +impl MaxRectsBin { + pub fn new(width: u32, height: u32) -> Self { + MaxRectsBin { + bin_width: width, + bin_height: height, used: Vec::new(), - free: vec![Rect::new(0, 0, DEFAULT_BIN_SIZE, DEFAULT_BIN_SIZE)], + free: vec![Rect::new(0, 0, width, height)], + oversized: false, + } + } + + pub fn oversized(dimensions: (u32, u32), index: usize) -> Self { + let used_rect = Rect::new(0, 0, dimensions.0, dimensions.1); + + MaxRectsBin { + bin_width: dimensions.0, + bin_height: dimensions.1, + used: vec![(used_rect, index)], + free: vec![], + oversized: true, } } @@ -87,10 +121,15 @@ impl MaxRectsBins { // Score all rects and sort them by their score, best score first let sorted = sprites .iter() - .map(|it| (self.score_rect(it.dimensions.0, it.dimensions.1), *it)) + .filter_map(|sprite| { + match self.score_rect(sprite.dimensions.0, sprite.dimensions.1) { + ScoreResult::NoFit => None, + ScoreResult::FitFound(score) => Some((score, *sprite)), + } + }) .collect::>(); - let next = { + let (score, sprite) = { // Find out if there's multiple with the best score let best_scored = sorted .iter() @@ -109,16 +148,16 @@ impl MaxRectsBins { } }; - self.place_rect(&next.0.placement, &next.1); - sprites.retain(|it| it.id != next.1.id); + self.place_rect(score.placement, *sprite, sprite.id); + sprites.retain(|s| s.id != sprite.id); } } - fn score_rect(&self, width: u32, height: u32) -> RectScore { - use std::cmp::{min, max}; + fn score_rect(&self, width: u32, height: u32) -> ScoreResult { + use std::cmp::{max, min}; - // For now, we always use bssf since it is the most efficient by itself - // TODO: Other strategies + global best choice + // We score by best short side fit, since it's the best performing + // strategy according to the reference implementation let mut best_short = std::u32::MAX; let mut best_long = std::u32::MAX; let mut placement = Rect::new(0, 0, 0, 0); @@ -130,25 +169,85 @@ impl MaxRectsBins { let short_side_fit = min(leftover_horiz, leftover_vert); let long_side_fit = max(leftover_horiz, leftover_vert); - if short_side_fit < best_short || - (short_side_fit == best_short && long_side_fit < best_long) { + if short_side_fit < best_short + || (short_side_fit == best_short && long_side_fit < best_long) + { best_short = short_side_fit; best_long = long_side_fit; placement = Rect::new(it.x, it.y, width, height); } }); - RectScore { - placement, - primary: best_short as i32, - secondary: best_long as i32, + // TODO(happenslol): This is kind of a primitive way to check, + // since it's directly translated from the reference implementation. + // This function can probably be improved a lot, style-wise + if placement.height == 0 { + ScoreResult::NoFit + } else { + ScoreResult::FitFound(RectScore { + placement, + primary: best_short as i32, + secondary: best_long as i32, + }) } } - fn find_position() {} + fn place_rect(&mut self, rect: Rect, sprite: SpriteData, sprite_id: usize) { + let mut to_process = self.free.len(); + let mut i = 0; - fn place_rect(&mut self, rect: &Rect, sprite: &SpriteData) { + while i < to_process { + if self.free[i].no_intersection(&rect) { + i += 1; + continue; + } + + let to_split = self.free.remove(i); + self.split_rect(to_split, rect); + to_process -= 1; + } + + self.prune_free_list(); + self.used.push((rect, sprite_id)); + } + + fn split_rect(&mut self, to_split: Rect, to_place: Rect) { + if to_place.x < to_split.x + to_split.width && to_place.x + to_place.width > to_split.x { + // New node at the top side of the placed node. + if to_place.y > to_split.y && to_place.y < to_split.y + to_split.height { + self.free.push(Rect { + y: to_place.y - to_split.y, + ..to_split + }) + } + if to_place.y + to_place.height < to_split.y + to_split.height { + self.free.push(Rect { + y: to_place.y + to_place.height, + height: to_split.y + to_split.height - (to_place.y + to_place.height), + ..to_split + }); + } + } + + if to_place.y < to_split.y + to_split.height && to_place.y + to_place.height > to_split.y { + // New node at the left side of the placed node. + if to_place.x > to_split.x && to_place.x < to_split.x + to_split.width { + self.free.push(Rect { + width: to_place.x - to_split.x, + ..to_split + }); + } + + // New node at the right side of the placed node. + if to_place.x + to_place.width < to_split.x + to_split.width { + self.free.push(Rect { + x: to_place.x + to_place.width, + width: to_split.x + to_split.width - (to_place.x + to_place.width), + ..to_split + }); + } + } } fn prune_free_list(&mut self) { From fb971fff442d554b411ccbe7fe782c64beef7ee4 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Fri, 28 Jun 2019 02:00:56 +0200 Subject: [PATCH 04/13] Implement bin distribution --- sheep/src/pack/maxrects.rs | 137 ++++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 41 deletions(-) diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index d162be2..07ea04f 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -2,31 +2,64 @@ use {Packer, PackerResult, SpriteAnchor, SpriteData}; pub struct MaxrectsPacker; +#[derive(Copy, Clone)] pub struct MaxrectsOptions { preferred_width: u32, preferred_height: u32, } +impl Default for MaxrectsOptions { + fn default() -> Self { + MaxrectsOptions { + preferred_width: 4096, + preferred_height: 4096, + } + } +} + +impl MaxrectsOptions { + pub fn preferred_width(mut self, width: u32) -> Self { + self.preferred_width = width; + self + } + + pub fn preferred_height(mut self, height: u32) -> Self { + self.preferred_height = height; + self + } +} + impl Packer for MaxrectsPacker { type Options = MaxrectsOptions; fn pack(sprites: &[SpriteData], options: MaxrectsOptions) -> Vec { - let mut bins = vec![MaxRectsBin::new( - options.preferred_width, - options.preferred_height, - )]; - + let mut bins = Vec::new(); let mut oversized = Vec::new(); - for (i, sprite) in sprites.iter().enumerate() { - if sprite.dimensions.0 > options.preferred_width - || sprite.dimensions.1 > options.preferred_height - { - oversized.push(MaxRectsBin::oversized(sprite.dimensions, i)); - continue; - } + // First, filter out all oversized sprites + let mut sprites = sprites + .iter() + .enumerate() + .filter(|(i, sprite)| { + if sprite.dimensions.0 > options.preferred_width + || sprite.dimensions.1 > options.preferred_height + { + oversized.push(MaxRectsBin::oversized(sprite.dimensions, *i)); + false + } else { + true + } + }) + .map(|(_, sprite)| *sprite) + .collect::>(); - for bin in bins.iter() {} + // Now, keep inserting as many as possible into each bin until + // all sprites have been placed. Since all oversized rects have + // already been filtered out, this will always terminate. + while !sprites.is_empty() { + let mut bin = MaxRectsBin::new(options.preferred_width, options.preferred_height); + sprites = bin.insert_sprites(&sprites); + bins.push(bin); } vec![PackerResult { @@ -75,6 +108,8 @@ enum ScoreResult { FitFound(RectScore), } +// NOTE(happenslol): The score represents the leftover +// space in case of a placement, thus _lower is better_ #[derive(Debug, Clone, Copy, PartialEq)] struct RectScore { placement: Rect, @@ -114,12 +149,13 @@ impl MaxRectsBin { } } - pub fn insert_sprites(&mut self, sprites: &[SpriteData]) { + pub fn insert_sprites(&mut self, sprites: &[SpriteData]) -> Vec { let mut sprites = sprites.iter().cloned().collect::>(); + let mut placed = Vec::new(); while !sprites.is_empty() { // Score all rects and sort them by their score, best score first - let sorted = sprites + let mut placeable = sprites .iter() .filter_map(|sprite| { match self.score_rect(sprite.dimensions.0, sprite.dimensions.1) { @@ -129,11 +165,18 @@ impl MaxRectsBin { }) .collect::>(); + // If the placeable list is empty at this point, we can break out and + // return all SpriteDatas we were not able to place + if placeable.is_empty() { + break; + } + + placeable.sort_by_key(|(score, _)| score.primary); let (score, sprite) = { // Find out if there's multiple with the best score - let best_scored = sorted + let best_scored = placeable .iter() - .filter(|(score, _)| score.primary == sorted[0].0.primary) + .filter(|(score, _)| score.primary == placeable[0].0.primary) .collect::>(); // If not, we have found the next best fit! Othweise, take the @@ -150,10 +193,13 @@ impl MaxRectsBin { self.place_rect(score.placement, *sprite, sprite.id); sprites.retain(|s| s.id != sprite.id); + placed.push(sprite.id); } + + sprites } - fn score_rect(&self, width: u32, height: u32) -> ScoreResult { + pub fn score_rect(&self, width: u32, height: u32) -> ScoreResult { use std::cmp::{max, min}; // We score by best short side fit, since it's the best performing @@ -207,7 +253,7 @@ impl MaxRectsBin { to_process -= 1; } - self.prune_free_list(); + remove_redundant_rects(&mut self.free); self.used.push((rect, sprite_id)); } @@ -249,31 +295,40 @@ impl MaxRectsBin { } } } +} - fn prune_free_list(&mut self) { - // TODO(happenslol): This is really ugly, since I haven't found - // a way to either modify the array while iterating over it, or - // modify the iterator variable while going over a range. - // I'm 99% sure there's a better way to do this. - let mut i = 0; - 'outer: while i < self.free.len() { - let mut j = i + 1; - - 'inner: while j < self.free.len() { - if self.free[j].contains(&self.free[i]) { - self.free.remove(i); - i -= 1; - break 'inner; - } +fn remove_redundant_rects(rects: &mut Vec) { + let mut i = 0; + while let Some(next) = rects.get(i).cloned() { + // check if it's contained by any other rect + if rects[i + 1..].iter().any(|s| s.contains(&next)) { + // if so, discard it and keep going + rects.swap_remove(i); + continue; + } - if self.free[i].contains(&self.free[j]) { - self.free.remove(j); - } else { - j += 1; - } + // otherwise, prune all unprocessed rects that are + // contained by our rect and accept it + for j in (i..rects.len()).rev() { + if rects[j].contains(&next) { + rects.swap_remove(j); } - - i += 1; } + + i += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pack_regular() { + let sprites = (0..10_000) + .map(|i| SpriteData::new(i, (100, 100))) + .collect::>(); + + let result = MaxrectsPacker::pack(&sprites, Default::default()); } } From b3431e4f7a0e6c8b88c60bc0a9db5685739c1dd8 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Fri, 28 Jun 2019 05:15:31 +0200 Subject: [PATCH 05/13] Implement bin shrinking --- sheep/examples/simple_pack/main.rs | 2 +- sheep/src/lib.rs | 8 +++-- sheep/src/pack/maxrects.rs | 47 ++++++++++++++++++++++++++---- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/sheep/examples/simple_pack/main.rs b/sheep/examples/simple_pack/main.rs index 0f3efa8..b606e40 100644 --- a/sheep/examples/simple_pack/main.rs +++ b/sheep/examples/simple_pack/main.rs @@ -27,7 +27,7 @@ fn main() { // Do the actual packing! 4 defines the stride, since we're using rgba8 we // have 4 bytes per pixel. - let results = sheep::pack::(sprites, 4, ()); + let results = sheep::pack::(sprites, 4, Default::default()); // SimplePacker always returns a single result. Other packers can return // multiple sheets; should they, for example, choose to enforce a maximum diff --git a/sheep/src/lib.rs b/sheep/src/lib.rs index 8f6ca1d..608eac4 100644 --- a/sheep/src/lib.rs +++ b/sheep/src/lib.rs @@ -11,7 +11,11 @@ mod sprite; pub use { format::Format, - pack::{simple::SimplePacker, Packer, PackerResult}, + pack::{ + Packer, PackerResult, + simple::SimplePacker, + maxrects::{MaxrectsPacker, MaxrectsOptions}, + }, sprite::{InputSprite, Sprite, SpriteAnchor, SpriteData}, }; @@ -21,7 +25,7 @@ pub use format::named::AmethystNamedFormat; use sprite::{create_pixel_buffer, write_sprite}; -#[allow(dead_code)] +#[derive(Debug, Clone)] pub struct SpriteSheet { pub bytes: Vec, pub stride: usize, diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index 07ea04f..b7054fb 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -62,10 +62,10 @@ impl Packer for MaxrectsPacker { bins.push(bin); } - vec![PackerResult { - dimensions: (0, 0), - anchors: Vec::new(), - }] + bins.extend(oversized.into_iter()); + let result = bins.into_iter().map(|bin| bin.to_result()).collect::>(); + + result } } @@ -149,6 +149,41 @@ impl MaxRectsBin { } } + pub fn to_result(&self) -> PackerResult { + let anchors = self.used + .iter() + .map(|(rect, id)| SpriteAnchor { + id: *id, + position: (rect.x, rect.y), + dimensions: (rect.width, rect.height), + }) + .collect::>(); + + let null_anchor = SpriteAnchor { id: 0, position: (0, 0), dimensions: (0, 0) }; + + let (w, h) = { + let max_x = anchors + .iter() + .max_by_key(|a| a.position.0) + .unwrap_or(&null_anchor); + + let max_y = anchors + .iter() + .max_by_key(|a| a.position.1) + .unwrap_or(&null_anchor); + + let w = max_x.position.0 + max_x.dimensions.0; + let h = max_y.position.1 + max_y.dimensions.1; + + (w, h) + }; + + PackerResult { + dimensions: (w, h), + anchors, + } + } + pub fn insert_sprites(&mut self, sprites: &[SpriteData]) -> Vec { let mut sprites = sprites.iter().cloned().collect::>(); let mut placed = Vec::new(); @@ -191,7 +226,7 @@ impl MaxRectsBin { } }; - self.place_rect(score.placement, *sprite, sprite.id); + self.place_rect(score.placement, sprite.id); sprites.retain(|s| s.id != sprite.id); placed.push(sprite.id); } @@ -238,7 +273,7 @@ impl MaxRectsBin { } } - fn place_rect(&mut self, rect: Rect, sprite: SpriteData, sprite_id: usize) { + fn place_rect(&mut self, rect: Rect, sprite_id: usize) { let mut to_process = self.free.len(); let mut i = 0; From b157d3818c8af564ece2a2b6262ce90aa4348ea5 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Fri, 28 Jun 2019 15:06:39 +0200 Subject: [PATCH 06/13] Add tests and benchmarks --- sheep/src/lib.rs | 7 ++- sheep/src/pack/maxrects.rs | 100 +++++++++++++++++++++++++++++-------- 2 files changed, 84 insertions(+), 23 deletions(-) diff --git a/sheep/src/lib.rs b/sheep/src/lib.rs index 608eac4..9841095 100644 --- a/sheep/src/lib.rs +++ b/sheep/src/lib.rs @@ -1,3 +1,6 @@ +#![feature(test)] +extern crate test; + #[cfg(feature = "amethyst")] extern crate serde; @@ -12,9 +15,9 @@ mod sprite; pub use { format::Format, pack::{ - Packer, PackerResult, + maxrects::{MaxrectsOptions, MaxrectsPacker}, simple::SimplePacker, - maxrects::{MaxrectsPacker, MaxrectsOptions}, + Packer, PackerResult, }, sprite::{InputSprite, Sprite, SpriteAnchor, SpriteData}, }; diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index b7054fb..870b841 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -63,7 +63,10 @@ impl Packer for MaxrectsPacker { } bins.extend(oversized.into_iter()); - let result = bins.into_iter().map(|bin| bin.to_result()).collect::>(); + let result = bins + .into_iter() + .map(|bin| bin.to_result()) + .collect::>(); result } @@ -150,7 +153,8 @@ impl MaxRectsBin { } pub fn to_result(&self) -> PackerResult { - let anchors = self.used + let anchors = self + .used .iter() .map(|(rect, id)| SpriteAnchor { id: *id, @@ -159,24 +163,17 @@ impl MaxRectsBin { }) .collect::>(); - let null_anchor = SpriteAnchor { id: 0, position: (0, 0), dimensions: (0, 0) }; - - let (w, h) = { - let max_x = anchors - .iter() - .max_by_key(|a| a.position.0) - .unwrap_or(&null_anchor); - - let max_y = anchors - .iter() - .max_by_key(|a| a.position.1) - .unwrap_or(&null_anchor); - - let w = max_x.position.0 + max_x.dimensions.0; - let h = max_y.position.1 + max_y.dimensions.1; + let w = anchors + .iter() + .map(|a| a.position.0 + a.dimensions.0) + .max() + .unwrap_or(0); - (w, h) - }; + let h = anchors + .iter() + .map(|a| a.position.1 + a.dimensions.1) + .max() + .unwrap_or(0); PackerResult { dimensions: (w, h), @@ -357,13 +354,74 @@ fn remove_redundant_rects(rects: &mut Vec) { #[cfg(test)] mod tests { use super::*; + use crate::test::{self, Bencher}; #[test] fn pack_regular() { - let sprites = (0..10_000) + let mut sprites = (0..10) + .map(|i| SpriteData::new(i, (10, 10))) + .collect::>(); + + let options = MaxrectsOptions::default() + .preferred_width(10 * 10) + .preferred_height(10 * 10); + + let result = MaxrectsPacker::pack(&sprites, options); + let first = result.iter().next().expect("should have 1 result"); + + assert_eq!(result.len(), 1); + + // They'll all be packed into 1 row in this example, so they output + // will be shrunk to fit the entire width plus 1 row. + assert_eq!(first.dimensions.0, 10 * 10); + assert_eq!(first.dimensions.1, 10); + + sprites.push(SpriteData::new(11, (10, 20))); + let result = MaxrectsPacker::pack(&sprites, options); + let first = result.iter().next().expect("should have 1 result"); + + assert_eq!(first.dimensions.0, 10 * 10); + assert_eq!(first.dimensions.1, 20); + } + + #[test] + fn pack_oversized() { + let oversized = (0..1000) .map(|i| SpriteData::new(i, (100, 100))) .collect::>(); - let result = MaxrectsPacker::pack(&sprites, Default::default()); + let options = MaxrectsOptions::default() + .preferred_width(50) + .preferred_height(50); + + let result = MaxrectsPacker::pack(&oversized, options); + + assert_eq!(result.len(), oversized.len()); + for bin in result { + assert_eq!(bin.dimensions.0, 100); + assert_eq!(bin.dimensions.1, 100); + } + } + + #[bench] + fn bench_pack(b: &mut Bencher) { + let mut sprites = (0..1000) + .map(|i| SpriteData::new(i, (100, 100))) + .collect::>(); + + let smaller_sprites = (0..1000) + .map(|i| SpriteData::new(i, (80, 80))) + .collect::>(); + + sprites.extend(smaller_sprites.into_iter()); + + let options = MaxrectsOptions::default() + .preferred_width(1000 * 100) + .preferred_height(1000 * 100); + + b.iter(|| { + let result = MaxrectsPacker::pack(&sprites, options); + let _ = test::black_box(&result); + }); } } From d04691bedf5837717d6e609f4c1811fcff7e77ea Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Fri, 28 Jun 2019 16:38:56 +0200 Subject: [PATCH 07/13] Switch internal rect representation to min/max --- sheep/src/pack/maxrects.rs | 117 ++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index 870b841..9749d12 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -74,34 +74,43 @@ impl Packer for MaxrectsPacker { #[derive(Debug, Clone, Copy, PartialEq)] struct Rect { - pub x: u32, - pub y: u32, - pub width: u32, - pub height: u32, + pub min_x: u32, + pub min_y: u32, + pub max_x: u32, + pub max_y: u32, } impl Rect { - pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self { + pub fn xywh(x: u32, y: u32, width: u32, height: u32) -> Self { Rect { - x, - y, - width, - height, + min_x: x, + min_y: y, + max_x: x + width, + max_y: y + height, + } + } + + pub fn new(min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> Self { + Rect { + min_x, + min_y, + max_x, + max_y, } } pub fn contains(&self, other: &Rect) -> bool { - self.x >= other.x - && self.y >= other.y - && self.x + self.width <= other.x + other.width - && self.y + self.height <= other.y + other.height + self.min_x >= other.min_x + && self.min_y >= other.min_y + && self.max_x <= other.max_x + && self.max_y <= other.max_y } pub fn no_intersection(&self, other: &Rect) -> bool { - self.x >= other.x + other.width - || self.x + self.width <= other.x - || self.y >= other.y + other.height - || self.y + self.height <= other.y + self.min_x >= other.max_x + || self.max_x <= other.min_x + || self.min_y >= other.max_y + || self.max_y <= other.min_y } } @@ -135,13 +144,13 @@ impl MaxRectsBin { bin_width: width, bin_height: height, used: Vec::new(), - free: vec![Rect::new(0, 0, width, height)], + free: vec![Rect::xywh(0, 0, width, height)], oversized: false, } } pub fn oversized(dimensions: (u32, u32), index: usize) -> Self { - let used_rect = Rect::new(0, 0, dimensions.0, dimensions.1); + let used_rect = Rect::xywh(0, 0, dimensions.0, dimensions.1); MaxRectsBin { bin_width: dimensions.0, @@ -158,8 +167,8 @@ impl MaxRectsBin { .iter() .map(|(rect, id)| SpriteAnchor { id: *id, - position: (rect.x, rect.y), - dimensions: (rect.width, rect.height), + position: (rect.min_x, rect.min_y), + dimensions: (rect.max_x - rect.min_x, rect.max_y - rect.min_y), }) .collect::>(); @@ -239,10 +248,14 @@ impl MaxRectsBin { let mut best_short = std::u32::MAX; let mut best_long = std::u32::MAX; let mut placement = Rect::new(0, 0, 0, 0); + let mut fit_found = false; + + for rect in &self.free { + let other_width = (rect.max_x - rect.min_x) as i32; + let other_height = (rect.max_y - rect.min_y) as i32; - self.free.iter().for_each(|it| { - let leftover_horiz = (it.width as i32 - width as i32).abs() as u32; - let leftover_vert = (it.height as i32 - height as i32).abs() as u32; + let leftover_horiz = (other_width - width as i32).abs() as u32; + let leftover_vert = (other_height - height as i32).abs() as u32; let short_side_fit = min(leftover_horiz, leftover_vert); let long_side_fit = max(leftover_horiz, leftover_vert); @@ -252,14 +265,12 @@ impl MaxRectsBin { { best_short = short_side_fit; best_long = long_side_fit; - placement = Rect::new(it.x, it.y, width, height); + placement = Rect::xywh(rect.min_x, rect.min_y, width, height); + fit_found = true; } - }); + } - // TODO(happenslol): This is kind of a primitive way to check, - // since it's directly translated from the reference implementation. - // This function can probably be improved a lot, style-wise - if placement.height == 0 { + if !fit_found { ScoreResult::NoFit } else { ScoreResult::FitFound(RectScore { @@ -289,40 +300,52 @@ impl MaxRectsBin { self.used.push((rect, sprite_id)); } - fn split_rect(&mut self, to_split: Rect, to_place: Rect) { - if to_place.x < to_split.x + to_split.width && to_place.x + to_place.width > to_split.x { + fn split_rect(&mut self, split: Rect, place: Rect) { + if place.min_x < split.max_x && place.max_x > split.min_x { // New node at the top side of the placed node. - if to_place.y > to_split.y && to_place.y < to_split.y + to_split.height { + if place.min_y > split.min_y && place.min_y < split.max_y { + let height = split.max_y - split.min_y; + let new_min_y = place.min_y - split.min_y; + self.free.push(Rect { - y: to_place.y - to_split.y, - ..to_split + min_y: new_min_y, + max_y: new_min_y + height, + ..split }) } - if to_place.y + to_place.height < to_split.y + to_split.height { + if place.max_y < split.max_y { + let new_min_y = place.max_y; + let height = split.max_y - place.max_y; + self.free.push(Rect { - y: to_place.y + to_place.height, - height: to_split.y + to_split.height - (to_place.y + to_place.height), - ..to_split + min_y: new_min_y, + max_y: new_min_y + height, + ..split }); } } - if to_place.y < to_split.y + to_split.height && to_place.y + to_place.height > to_split.y { + if place.min_y < split.max_y && place.max_y > split.min_y { // New node at the left side of the placed node. - if to_place.x > to_split.x && to_place.x < to_split.x + to_split.width { + if place.min_x > split.min_x && place.min_x < split.max_x { + let width = place.min_x - split.min_x; + self.free.push(Rect { - width: to_place.x - to_split.x, - ..to_split + max_x: split.min_x + width, + ..split }); } // New node at the right side of the placed node. - if to_place.x + to_place.width < to_split.x + to_split.width { + if place.max_x < split.max_x { + let new_min_x = place.max_x; + let width = split.max_x - place.max_x; + self.free.push(Rect { - x: to_place.x + to_place.width, - width: to_split.x + to_split.width - (to_place.x + to_place.width), - ..to_split + min_x: new_min_x, + max_x: new_min_x + width, + ..split }); } } From 9334b59fed1d58f5be362ca564b93376be43446a Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Fri, 28 Jun 2019 17:55:08 +0200 Subject: [PATCH 08/13] Fix remove redundant --- sheep/src/pack/maxrects.rs | 43 +++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index 9749d12..496d07c 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -100,10 +100,10 @@ impl Rect { } pub fn contains(&self, other: &Rect) -> bool { - self.min_x >= other.min_x - && self.min_y >= other.min_y - && self.max_x <= other.max_x - && self.max_y <= other.max_y + self.min_x <= other.min_x + && self.min_y <= other.min_y + && self.max_x >= other.max_x + && self.max_y >= other.max_y } pub fn no_intersection(&self, other: &Rect) -> bool { @@ -364,8 +364,8 @@ fn remove_redundant_rects(rects: &mut Vec) { // otherwise, prune all unprocessed rects that are // contained by our rect and accept it - for j in (i..rects.len()).rev() { - if rects[j].contains(&next) { + for j in ((i + 1)..rects.len()).rev() { + if next.contains(&rects[j]) { rects.swap_remove(j); } } @@ -379,6 +379,24 @@ mod tests { use super::*; use crate::test::{self, Bencher}; + #[test] + fn remove_redundant() { + let mut rects = Vec::new(); + for i in 0..10 { + rects.push(Rect::xywh(i * 10, 0, 10, 10)); + rects.push(Rect::xywh(i * 10 + 2, 2, 6, 6)); + } + + assert_eq!(rects.len(), 20); + remove_redundant_rects(&mut rects); + assert_eq!(rects.len(), 10); + + for rect in &rects { + assert_eq!((rect.max_x - rect.min_x), 10); + assert_eq!((rect.max_y - rect.min_y), 10); + } + } + #[test] fn pack_regular() { let mut sprites = (0..10) @@ -394,17 +412,18 @@ mod tests { assert_eq!(result.len(), 1); - // They'll all be packed into 1 row in this example, so they output - // will be shrunk to fit the entire width plus 1 row. - assert_eq!(first.dimensions.0, 10 * 10); - assert_eq!(first.dimensions.1, 10); + // They'll all be packed into 1 column in this example, so they output + // will be shrunk to fit the entire width plus 1 column. + assert_eq!(first.dimensions.0, 10); + assert_eq!(first.dimensions.1, 10 * 10); + // The new sprite will bu pushed to the next line sprites.push(SpriteData::new(11, (10, 20))); let result = MaxrectsPacker::pack(&sprites, options); let first = result.iter().next().expect("should have 1 result"); - assert_eq!(first.dimensions.0, 10 * 10); - assert_eq!(first.dimensions.1, 20); + assert_eq!(first.dimensions.0, 30); + assert_eq!(first.dimensions.1, 100); } #[test] From 503c4bbf8b8b36c083d7e1b90eef430550c00d25 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Fri, 28 Jun 2019 18:22:59 +0200 Subject: [PATCH 09/13] Move bench to benches folder --- sheep/benches/maxrects.rs | 28 ++++++++++++++++++++++++++++ sheep/src/lib.rs | 3 --- sheep/src/pack/maxrects.rs | 23 ----------------------- 3 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 sheep/benches/maxrects.rs diff --git a/sheep/benches/maxrects.rs b/sheep/benches/maxrects.rs new file mode 100644 index 0000000..871694a --- /dev/null +++ b/sheep/benches/maxrects.rs @@ -0,0 +1,28 @@ +#![feature(test)] +extern crate test; +extern crate sheep; + +use test::Bencher; +use sheep::{Packer, SpriteData, MaxrectsPacker, MaxrectsOptions}; + +#[bench] +fn bench_pack(b: &mut Bencher) { + let mut sprites = (0..1000) + .map(|i| SpriteData::new(i, (100, 100))) + .collect::>(); + + let smaller_sprites = (0..1000) + .map(|i| SpriteData::new(i, (80, 80))) + .collect::>(); + + sprites.extend(smaller_sprites.into_iter()); + + let options = MaxrectsOptions::default() + .preferred_width(1000 * 100) + .preferred_height(1000 * 100); + + b.iter(|| { + let result = MaxrectsPacker::pack(&sprites, options); + let _ = test::black_box(&result); + }); +} \ No newline at end of file diff --git a/sheep/src/lib.rs b/sheep/src/lib.rs index 9841095..150b5aa 100644 --- a/sheep/src/lib.rs +++ b/sheep/src/lib.rs @@ -1,6 +1,3 @@ -#![feature(test)] -extern crate test; - #[cfg(feature = "amethyst")] extern crate serde; diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index 496d07c..7520036 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -377,7 +377,6 @@ fn remove_redundant_rects(rects: &mut Vec) { #[cfg(test)] mod tests { use super::*; - use crate::test::{self, Bencher}; #[test] fn remove_redundant() { @@ -444,26 +443,4 @@ mod tests { assert_eq!(bin.dimensions.1, 100); } } - - #[bench] - fn bench_pack(b: &mut Bencher) { - let mut sprites = (0..1000) - .map(|i| SpriteData::new(i, (100, 100))) - .collect::>(); - - let smaller_sprites = (0..1000) - .map(|i| SpriteData::new(i, (80, 80))) - .collect::>(); - - sprites.extend(smaller_sprites.into_iter()); - - let options = MaxrectsOptions::default() - .preferred_width(1000 * 100) - .preferred_height(1000 * 100); - - b.iter(|| { - let result = MaxrectsPacker::pack(&sprites, options); - let _ = test::black_box(&result); - }); - } } From 298c477f52870050944030b7de0db5738e11058a Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Sat, 6 Jul 2019 23:07:27 +0200 Subject: [PATCH 10/13] Change CLI to use maxrects by default --- sheep/src/pack/maxrects.rs | 2 +- sheep_cli/src/main.rs | 166 ++++++++++++++++++++----------------- 2 files changed, 92 insertions(+), 76 deletions(-) diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index 7520036..aeebd8b 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -291,7 +291,7 @@ impl MaxRectsBin { continue; } - let to_split = self.free.remove(i); + let to_split = self.free.swap_remove(i); self.split_rect(to_split, rect); to_process -= 1; } diff --git a/sheep_cli/src/main.rs b/sheep_cli/src/main.rs index 01bcbaf..ba18131 100644 --- a/sheep_cli/src/main.rs +++ b/sheep_cli/src/main.rs @@ -7,12 +7,15 @@ extern crate sheep; use clap::{App, AppSettings, Arg, SubCommand}; use image::RgbaImage; use serde::Serialize; -use sheep::{AmethystFormat, AmethystNamedFormat, Format, InputSprite, SimplePacker}; +use sheep::{AmethystFormat, AmethystNamedFormat, InputSprite, MaxrectsPacker, SimplePacker}; use std::str::FromStr; use std::{fs::File, io::prelude::*}; const DEFAULT_FORMAT: &'static str = "amethyst"; -const AVAILABLE_FORMATS: [&str; 2] = ["amethyst", "amethyst_named"]; +const DEFAULT_PACKER: &'static str = "maxrects"; + +const AVAILABLE_FORMATS: [&'static str; 2] = ["amethyst", "amethyst_named"]; +const AVAILABLE_PACKERS: [&'static str; 2] = ["simple", "maxrects"]; fn main() { let app = App::new("sheep") @@ -33,6 +36,16 @@ fn main() { .required(false) .default_value("out"), ) + .arg( + Arg::with_name("packer") + .help("Packing algorithm to use") + .possible_values(&AVAILABLE_PACKERS) + .short("p") + .long("packer") + .takes_value(true) + .required(false) + .default_value(DEFAULT_PACKER), + ) .arg( Arg::with_name("format") .help("Determines the fields present in the serialized output.") @@ -40,6 +53,7 @@ fn main() { .short("f") .long("format") .takes_value(true) + .required(false) .default_value(DEFAULT_FORMAT), ), ); @@ -57,89 +71,91 @@ fn main() { .value_of("output") .expect("Unreachable: param has default value"); - match matches.value_of("format") { - Some("amethyst_named") => { - let names = input - .iter() - .map(|path| { - std::path::PathBuf::from(&path) - .file_stem() - .and_then(|name| name.to_str()) - .map(|name| { - String::from_str(name) - .expect("could not parse string from file name") - }) - .expect("Failed to extract file name") - }) - .collect(); - - do_pack::(input, names) - .map(|(output_image, meta)| write_files(out, output_image, meta)) - .expect("Failed to pack sprites") - } - Some(DEFAULT_FORMAT) => do_pack::(input, ()) - .map(|(output_image, meta)| write_files(out, output_image, meta)) - .expect("Failed to pack sprites"), - _ => { - panic!("Unknown format"); - } + let sprites = load_images(&input); + + // NOTE(happenslol): By default, we're using rgba8 right now, + // so the stride is always 4 + let results = match matches.value_of("packer") { + Some("maxrects") => sheep::pack::(sprites, 4, Default::default()), + Some("simple") => sheep::pack::(sprites, 4, ()), + _ => panic!("Unknown packer"), }; - } - _ => {} - } -} -fn do_pack( - input: Vec, - options: F::Options, -) -> Result<(image::RgbaImage, F::Data), &'static str> -where - F: Format, -{ - let mut sprites = Vec::new(); - - for path in input { - let img = image::open(&path).expect("Failed to open image"); - let img_owned; - let img = { - if let Some(img) = img.as_rgba8() { - img - } else { - img_owned = img.to_rgba(); - &img_owned + if results.is_empty() { + panic!("No output was produced"); } - }; - let dimensions = img.dimensions(); - let bytes = img - .pixels() - .flat_map(|it| it.data.iter().map(|it| *it)) - .collect::>(); + let is_single_sheet = results.len() == 1; - let sprite = InputSprite { dimensions, bytes }; - sprites.push(sprite); - } + for (i, sheet) in results.iter().enumerate() { + let filename = if i == 0 && is_single_sheet { + String::from(out) + } else { + format!("{}-{:02}", out, i) + }; - // NOTE(happenslol): By default, we're using rgba8 right now, - // so the stride is always 4 - let results = sheep::pack::(sprites, 4, ()); + let outbuf = RgbaImage::from_vec( + sheet.dimensions.0, + sheet.dimensions.1, + sheet.bytes.clone(), + ) + .expect("Failed to create image from spritesheet"); + + match matches.value_of("format") { + Some("amethyst_named") => { + let names = get_filenames(&input); + let meta = sheep::encode::(&sheet, names); + write_files(&filename, outbuf, meta); + } + Some("amethyst") => { + let meta = sheep::encode::(&sheet, ()); + write_files(&filename, outbuf, meta); + } + _ => panic!("Unknown format"), + }; + } + } + _ => {} + } +} - // NOTE(happenslol): SimplePacker always outputs 1 sheet. - let sprite_sheet = results - .into_iter() - .next() - .expect("Should have returned a spritesheet"); +fn get_filenames(input: &[String]) -> Vec { + input + .iter() + .map(|path| { + std::path::PathBuf::from(&path) + .file_stem() + .and_then(|name| name.to_str()) + .map(|name| String::from_str(name).expect("could not parse string from file name")) + .expect("Failed to extract file name") + }) + .collect() +} - let meta = sheep::encode::(&sprite_sheet, options); +fn load_images(input: &[String]) -> Vec { + input + .iter() + .map(|path| { + let img = image::open(&path).expect("Failed to open image"); + let img_owned; + let img = { + if let Some(img) = img.as_rgba8() { + img + } else { + img_owned = img.to_rgba(); + &img_owned + } + }; - let outbuf = RgbaImage::from_vec( - sprite_sheet.dimensions.0, - sprite_sheet.dimensions.1, - sprite_sheet.bytes, - ) - .ok_or("Failed to construct image from sprite sheet bytes")?; + let dimensions = img.dimensions(); + let bytes = img + .pixels() + .flat_map(|it| it.data.iter().map(|it| *it)) + .collect::>(); - return Ok((outbuf, meta)); + InputSprite { dimensions, bytes } + }) + .collect() } fn write_files(output_path: &str, outbuf: RgbaImage, meta: S) { From dcdd6810e44e4cdbf982f44124a597da5ff1bf7e Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Sat, 6 Jul 2019 23:08:56 +0200 Subject: [PATCH 11/13] Bump version --- Cargo.lock | 6 +++--- sheep/Cargo.toml | 2 +- sheep_cli/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1de2027..4d1a324 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,7 +343,7 @@ dependencies = [ [[package]] name = "sheep" -version = "0.1.0" +version = "0.2.1" dependencies = [ "image 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)", "ron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -353,13 +353,13 @@ dependencies = [ [[package]] name = "sheep_cli" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)", "ron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "sheep 0.1.0", + "sheep 0.2.1", ] [[package]] diff --git a/sheep/Cargo.toml b/sheep/Cargo.toml index aa04e19..48eee9c 100644 --- a/sheep/Cargo.toml +++ b/sheep/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sheep" -version = "0.1.0" +version = "0.2.1" authors = ["Hilmar Wiegand "] [features] diff --git a/sheep_cli/Cargo.toml b/sheep_cli/Cargo.toml index f76694a..2f87f37 100644 --- a/sheep_cli/Cargo.toml +++ b/sheep_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sheep_cli" -version = "0.1.0" +version = "0.2.0" authors = ["Hilmar Wiegand "] description = "Modular and lightweight spritesheet packer" From c23b29cf18bae9c2ed259eab621aa7ce7eeeec28 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Sat, 13 Jul 2019 14:51:07 +0200 Subject: [PATCH 12/13] Add options parser --- sheep/src/pack/maxrects.rs | 44 +++++++++++++++++++++++++------------- sheep_cli/src/main.rs | 37 +++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/sheep/src/pack/maxrects.rs b/sheep/src/pack/maxrects.rs index aeebd8b..6783d1e 100644 --- a/sheep/src/pack/maxrects.rs +++ b/sheep/src/pack/maxrects.rs @@ -4,27 +4,27 @@ pub struct MaxrectsPacker; #[derive(Copy, Clone)] pub struct MaxrectsOptions { - preferred_width: u32, - preferred_height: u32, + max_width: u32, + max_height: u32, } impl Default for MaxrectsOptions { fn default() -> Self { MaxrectsOptions { - preferred_width: 4096, - preferred_height: 4096, + max_width: 4096, + max_height: 4096, } } } impl MaxrectsOptions { - pub fn preferred_width(mut self, width: u32) -> Self { - self.preferred_width = width; + pub fn max_width(mut self, width: u32) -> Self { + self.max_width = width; self } - pub fn preferred_height(mut self, height: u32) -> Self { - self.preferred_height = height; + pub fn max_height(mut self, height: u32) -> Self { + self.max_height = height; self } } @@ -41,8 +41,8 @@ impl Packer for MaxrectsPacker { .iter() .enumerate() .filter(|(i, sprite)| { - if sprite.dimensions.0 > options.preferred_width - || sprite.dimensions.1 > options.preferred_height + if sprite.dimensions.0 > options.max_width + || sprite.dimensions.1 > options.max_height { oversized.push(MaxRectsBin::oversized(sprite.dimensions, *i)); false @@ -57,7 +57,7 @@ impl Packer for MaxrectsPacker { // all sprites have been placed. Since all oversized rects have // already been filtered out, this will always terminate. while !sprites.is_empty() { - let mut bin = MaxRectsBin::new(options.preferred_width, options.preferred_height); + let mut bin = MaxRectsBin::new(options.max_width, options.max_height); sprites = bin.insert_sprites(&sprites); bins.push(bin); } @@ -403,8 +403,8 @@ mod tests { .collect::>(); let options = MaxrectsOptions::default() - .preferred_width(10 * 10) - .preferred_height(10 * 10); + .max_width(10 * 10) + .max_height(10 * 10); let result = MaxrectsPacker::pack(&sprites, options); let first = result.iter().next().expect("should have 1 result"); @@ -425,6 +425,20 @@ mod tests { assert_eq!(first.dimensions.1, 100); } + #[test] + fn pack_multiple() { + let sprites = (0..500) + .map(|i| SpriteData::new(i, (10, 10))) + .collect::>(); + + let options = MaxrectsOptions::default() + .max_width(10 * 10) + .max_height(10 * 10); + + let result = MaxrectsPacker::pack(&sprites, options); + assert_eq!(result.len(), 5); + } + #[test] fn pack_oversized() { let oversized = (0..1000) @@ -432,8 +446,8 @@ mod tests { .collect::>(); let options = MaxrectsOptions::default() - .preferred_width(50) - .preferred_height(50); + .max_width(50) + .max_height(50); let result = MaxrectsPacker::pack(&oversized, options); diff --git a/sheep_cli/src/main.rs b/sheep_cli/src/main.rs index ba18131..240553c 100644 --- a/sheep_cli/src/main.rs +++ b/sheep_cli/src/main.rs @@ -7,7 +7,9 @@ extern crate sheep; use clap::{App, AppSettings, Arg, SubCommand}; use image::RgbaImage; use serde::Serialize; -use sheep::{AmethystFormat, AmethystNamedFormat, InputSprite, MaxrectsPacker, SimplePacker}; +use sheep::{ + AmethystFormat, AmethystNamedFormat, InputSprite, MaxrectsOptions, MaxrectsPacker, SimplePacker, +}; use std::str::FromStr; use std::{fs::File, io::prelude::*}; @@ -48,13 +50,22 @@ fn main() { ) .arg( Arg::with_name("format") - .help("Determines the fields present in the serialized output.") + .help("Determines the fields present in the serialized output") .possible_values(&AVAILABLE_FORMATS) .short("f") .long("format") .takes_value(true) .required(false) .default_value(DEFAULT_FORMAT), + ) + .arg( + Arg::with_name("options") + .help("Settings that will be passed to the selected packer") + .short("s") + .long("options") + .takes_value(true) + .multiple(true) + .required(false), ), ); @@ -76,7 +87,27 @@ fn main() { // NOTE(happenslol): By default, we're using rgba8 right now, // so the stride is always 4 let results = match matches.value_of("packer") { - Some("maxrects") => sheep::pack::(sprites, 4, Default::default()), + Some("maxrects") => { + let max_width = matches + .values_of("options") + .and_then(|mut options| options.find(|o| o.starts_with("max_width"))) + .and_then(|found| found.split("=").nth(1)) + .and_then(|value| value.parse::().ok()) + .unwrap_or(4096); + + let max_height = matches + .values_of("options") + .and_then(|mut options| options.find(|o| o.starts_with("max_height"))) + .and_then(|found| found.split("=").nth(1)) + .and_then(|value| value.parse::().ok()) + .unwrap_or(4096); + + let options = MaxrectsOptions::default() + .max_width(max_width) + .max_height(max_height); + + sheep::pack::(sprites, 4, options) + } Some("simple") => sheep::pack::(sprites, 4, ()), _ => panic!("Unknown packer"), }; From 0f51e9d56456418d4b5faaebb6034c166f2063d3 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Sat, 13 Jul 2019 15:38:54 +0200 Subject: [PATCH 13/13] Update readme --- README.md | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ef63679..25052d1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,22 @@ The project is in heavy development and the API might change a few times until w ## Usage -To use the CLI, simple use `cargo run -- ...`, usage hints are provided. For an example on how to use the library, please see the `simple_pack` example in the `sheep/examples` directory. +To use the CLI, simply install it with cargo: + +``` +cargo install sheep_cli +``` + +Usagen hints are provided. To see all options, simply run the command with no arguments. Options can be passed to the packers using the `--options` flag, as space separated `key=value` pairs. +By default, the `maxrects` packer will be used, see [packers](#Packers) for more information. + +**Example:** + +``` +sheep pack --options max_width=1024 max_height=1024 sprites/*.png +``` + +If you want to use the CLI from source, simple clone the repo and run `cargo run -- ...`. For an example on how to use the library directly, please see the `simple_pack` example in the `sheep/examples` directory. ## Implementing your own `Packer` and `Format` @@ -60,16 +75,29 @@ let sprite_sheet = sheep::pack::(sprites, 4); let meta = sheep::encode::(&sprite_sheet); ``` +## Packers + +Right now, there are two implementations to use: + +- maxrects (**recommended**) + +Implementation of the maxrects sprite packing algorithm. The paper and original implementation used as a reference for this can be found [here](https://github.com/juj/RectangleBinPack). This algorithm should yield optimal results in most scenarios. + +- simple + +A naive implementation that will sort the sprites by area and then pack them all into a single texture. This won't scale very well since you can't limit the maximum size of the resulting sprite sheet, but can be quicker than maxrects in simple scenarios. + ## Roadmap Here are the planned features for `sheep`: -* Support for multiple output textures (bins) -* Smart output texture sizing -* More packing algorithms - * MAXRECTS - * Skyline -* More meta formats +- ~~Support for multiple output textures (bins)~~ +- ~~Smart output texture sizing~~ +- More packing algorithms + - ~~MAXRECTS~~ + - Skyline +- More meta formats +- More image formats ## License