schematic_mesher/atlas/
builder.rs

1//! Texture atlas builder using simple row packing.
2
3use crate::error::{MesherError, Result};
4use crate::resource_pack::TextureData;
5use std::collections::HashMap;
6
7/// A region within the texture atlas.
8#[derive(Debug, Clone, Copy)]
9pub struct AtlasRegion {
10    /// U coordinate of the left edge (0-1).
11    pub u_min: f32,
12    /// V coordinate of the top edge (0-1).
13    pub v_min: f32,
14    /// U coordinate of the right edge (0-1).
15    pub u_max: f32,
16    /// V coordinate of the bottom edge (0-1).
17    pub v_max: f32,
18}
19
20impl AtlasRegion {
21    /// Get the width of this region in UV space.
22    pub fn width(&self) -> f32 {
23        self.u_max - self.u_min
24    }
25
26    /// Get the height of this region in UV space.
27    pub fn height(&self) -> f32 {
28        self.v_max - self.v_min
29    }
30
31    /// Transform a local UV coordinate (0-1) to atlas coordinate.
32    pub fn transform_uv(&self, u: f32, v: f32) -> [f32; 2] {
33        [
34            self.u_min + u * self.width(),
35            self.v_min + v * self.height(),
36        ]
37    }
38}
39
40/// A built texture atlas.
41#[derive(Debug)]
42pub struct TextureAtlas {
43    /// Width of the atlas in pixels.
44    pub width: u32,
45    /// Height of the atlas in pixels.
46    pub height: u32,
47    /// RGBA pixel data.
48    pub pixels: Vec<u8>,
49    /// Mapping from texture path to atlas region.
50    pub regions: HashMap<String, AtlasRegion>,
51}
52
53impl TextureAtlas {
54    /// Get the region for a texture.
55    pub fn get_region(&self, texture_path: &str) -> Option<&AtlasRegion> {
56        self.regions.get(texture_path)
57    }
58
59    /// Check if the atlas contains a texture.
60    pub fn contains(&self, texture_path: &str) -> bool {
61        self.regions.contains_key(texture_path)
62    }
63
64    /// Create an empty atlas.
65    pub fn empty() -> Self {
66        Self {
67            width: 16,
68            height: 16,
69            pixels: vec![255; 16 * 16 * 4], // White
70            regions: HashMap::new(),
71        }
72    }
73
74    /// Export the atlas as PNG bytes.
75    pub fn to_png(&self) -> Result<Vec<u8>> {
76        use image::{ImageBuffer, Rgba};
77
78        let img: ImageBuffer<Rgba<u8>, _> =
79            ImageBuffer::from_raw(self.width, self.height, self.pixels.clone())
80                .ok_or_else(|| MesherError::AtlasBuild("Failed to create image buffer".to_string()))?;
81
82        let mut bytes = Vec::new();
83        let mut cursor = std::io::Cursor::new(&mut bytes);
84
85        img.write_to(&mut cursor, image::ImageFormat::Png)
86            .map_err(|e| MesherError::AtlasBuild(format!("Failed to encode PNG: {}", e)))?;
87
88        Ok(bytes)
89    }
90}
91
92/// Builder for creating texture atlases.
93pub struct AtlasBuilder {
94    max_size: u32,
95    padding: u32,
96    textures: HashMap<String, TextureData>,
97}
98
99impl AtlasBuilder {
100    /// Create a new atlas builder.
101    pub fn new(max_size: u32, padding: u32) -> Self {
102        Self {
103            max_size,
104            padding,
105            textures: HashMap::new(),
106        }
107    }
108
109    /// Add a texture to the atlas.
110    pub fn add_texture(&mut self, path: String, texture: TextureData) {
111        self.textures.insert(path, texture);
112    }
113
114    /// Build the texture atlas using simple row packing.
115    pub fn build(self) -> Result<TextureAtlas> {
116        if self.textures.is_empty() {
117            return Ok(TextureAtlas::empty());
118        }
119
120        let padding = self.padding;
121        let max_size = self.max_size;
122
123        // Sort textures by height (tallest first) for better packing
124        let mut textures: Vec<_> = self.textures.into_iter().collect();
125        textures.sort_by(|a, b| b.1.height.cmp(&a.1.height));
126
127        // Calculate required atlas size
128        let total_area: u32 = textures
129            .iter()
130            .map(|(_, t)| (t.width + padding * 2) * (t.height + padding * 2))
131            .sum();
132
133        // Start with minimum size that could fit all textures
134        let min_size = (total_area as f64).sqrt().ceil() as u32;
135        let mut atlas_size = 64u32;
136        while atlas_size < min_size && atlas_size < max_size {
137            atlas_size *= 2;
138        }
139
140        // Try to pack at increasing sizes
141        loop {
142            if atlas_size > max_size {
143                return Err(MesherError::AtlasBuild(format!(
144                    "Failed to pack {} textures into {}x{} atlas",
145                    textures.len(),
146                    max_size,
147                    max_size
148                )));
149            }
150
151            if let Some((pixels, regions)) = try_pack(&textures, atlas_size, padding) {
152                return Ok(TextureAtlas {
153                    width: atlas_size,
154                    height: atlas_size,
155                    pixels,
156                    regions,
157                });
158            }
159
160            atlas_size *= 2;
161        }
162    }
163}
164
165/// Try to pack textures into an atlas of the given size.
166fn try_pack(
167    textures: &[(String, TextureData)],
168    atlas_size: u32,
169    padding: u32,
170) -> Option<(Vec<u8>, HashMap<String, AtlasRegion>)> {
171    let mut pixels = vec![0u8; (atlas_size * atlas_size * 4) as usize];
172    let mut regions = HashMap::new();
173
174    // Simple row-based packing
175    let mut current_x = 0u32;
176    let mut current_y = 0u32;
177    let mut row_height = 0u32;
178
179    for (path, texture) in textures {
180        let tex_width = texture.width + padding * 2;
181        let tex_height = texture.height + padding * 2;
182
183        // Check if we need to start a new row
184        if current_x + tex_width > atlas_size {
185            current_x = 0;
186            current_y += row_height;
187            row_height = 0;
188        }
189
190        // Check if we've run out of space
191        if current_y + tex_height > atlas_size {
192            return None;
193        }
194
195        // Place the texture
196        let x = current_x + padding;
197        let y = current_y + padding;
198
199        // Copy texture pixels to atlas
200        for ty in 0..texture.height {
201            for tx in 0..texture.width {
202                let src_idx = ((ty * texture.width + tx) * 4) as usize;
203                let dst_x = x + tx;
204                let dst_y = y + ty;
205                let dst_idx = ((dst_y * atlas_size + dst_x) * 4) as usize;
206
207                if src_idx + 4 <= texture.pixels.len() && dst_idx + 4 <= pixels.len() {
208                    pixels[dst_idx..dst_idx + 4]
209                        .copy_from_slice(&texture.pixels[src_idx..src_idx + 4]);
210                }
211            }
212        }
213
214        // Record the region
215        let region = AtlasRegion {
216            u_min: x as f32 / atlas_size as f32,
217            v_min: y as f32 / atlas_size as f32,
218            u_max: (x + texture.width) as f32 / atlas_size as f32,
219            v_max: (y + texture.height) as f32 / atlas_size as f32,
220        };
221        regions.insert(path.clone(), region);
222
223        // Update position
224        current_x += tex_width;
225        row_height = row_height.max(tex_height);
226    }
227
228    Some((pixels, regions))
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    fn create_test_texture(width: u32, height: u32, color: [u8; 4]) -> TextureData {
236        let pixels: Vec<u8> = (0..width * height)
237            .flat_map(|_| color.iter().copied())
238            .collect();
239        TextureData::new(width, height, pixels)
240    }
241
242    #[test]
243    fn test_empty_atlas() {
244        let builder = AtlasBuilder::new(256, 0);
245        let atlas = builder.build().unwrap();
246        assert_eq!(atlas.width, 16);
247        assert_eq!(atlas.height, 16);
248        assert!(atlas.regions.is_empty());
249    }
250
251    #[test]
252    fn test_single_texture_atlas() {
253        let mut builder = AtlasBuilder::new(256, 0);
254        builder.add_texture(
255            "test".to_string(),
256            create_test_texture(16, 16, [255, 0, 0, 255]),
257        );
258
259        let atlas = builder.build().unwrap();
260        assert!(atlas.regions.contains_key("test"));
261
262        let region = atlas.get_region("test").unwrap();
263        assert!(region.u_min >= 0.0);
264        assert!(region.u_max <= 1.0);
265        assert!(region.v_min >= 0.0);
266        assert!(region.v_max <= 1.0);
267    }
268
269    #[test]
270    fn test_multiple_textures() {
271        let mut builder = AtlasBuilder::new(256, 1);
272        builder.add_texture(
273            "red".to_string(),
274            create_test_texture(16, 16, [255, 0, 0, 255]),
275        );
276        builder.add_texture(
277            "green".to_string(),
278            create_test_texture(16, 16, [0, 255, 0, 255]),
279        );
280        builder.add_texture(
281            "blue".to_string(),
282            create_test_texture(16, 16, [0, 0, 255, 255]),
283        );
284
285        let atlas = builder.build().unwrap();
286        assert_eq!(atlas.regions.len(), 3);
287        assert!(atlas.contains("red"));
288        assert!(atlas.contains("green"));
289        assert!(atlas.contains("blue"));
290    }
291
292    #[test]
293    fn test_atlas_region_transform() {
294        let region = AtlasRegion {
295            u_min: 0.25,
296            v_min: 0.5,
297            u_max: 0.5,
298            v_max: 0.75,
299        };
300
301        let [u, v] = region.transform_uv(0.0, 0.0);
302        assert!((u - 0.25).abs() < 0.001);
303        assert!((v - 0.5).abs() < 0.001);
304
305        let [u, v] = region.transform_uv(1.0, 1.0);
306        assert!((u - 0.5).abs() < 0.001);
307        assert!((v - 0.75).abs() < 0.001);
308    }
309}