Skip to main content

tex_packer_core/
runtime_atlas.rs

1use crate::config::PackerConfig;
2use crate::error::{Result, TexPackerError};
3use crate::model::Frame;
4use crate::runtime::{AtlasSession, RuntimeStats, RuntimeStrategy};
5use image::{Rgba, RgbaImage};
6
7/// Region that needs to be updated on GPU texture.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct UpdateRegion {
10    /// Page ID that needs updating.
11    pub page_id: usize,
12    /// X coordinate of the region.
13    pub x: u32,
14    /// Y coordinate of the region.
15    pub y: u32,
16    /// Width of the region.
17    pub width: u32,
18    /// Height of the region.
19    pub height: u32,
20}
21
22impl UpdateRegion {
23    /// Create an empty update region.
24    pub fn empty() -> Self {
25        Self {
26            page_id: 0,
27            x: 0,
28            y: 0,
29            width: 0,
30            height: 0,
31        }
32    }
33
34    /// Check if this region is empty.
35    pub fn is_empty(&self) -> bool {
36        self.width == 0 || self.height == 0
37    }
38
39    /// Get the area of this region in pixels.
40    pub fn area(&self) -> u64 {
41        (self.width as u64) * (self.height as u64)
42    }
43}
44
45/// Runtime atlas with pixel data management.
46///
47/// This extends `AtlasSession` by managing actual pixel data in addition to geometry.
48/// Useful for game engines that need to dynamically update GPU textures.
49pub struct RuntimeAtlas {
50    session: AtlasSession,
51    pages: Vec<RgbaImage>,
52    background_color: Rgba<u8>,
53}
54
55impl RuntimeAtlas {
56    /// Create a new runtime atlas with pixel data management.
57    pub fn new(cfg: PackerConfig, strategy: RuntimeStrategy) -> Self {
58        Self {
59            session: AtlasSession::new(cfg, strategy),
60            pages: Vec::new(),
61            background_color: Rgba([0, 0, 0, 0]), // Transparent by default
62        }
63    }
64
65    /// Set the background color for new pages.
66    pub fn with_background_color(mut self, color: Rgba<u8>) -> Self {
67        self.background_color = color;
68        self
69    }
70
71    /// Append a texture with its pixel data.
72    /// Returns (page_id, frame, update_region).
73    pub fn append_with_image(
74        &mut self,
75        key: String,
76        image: &RgbaImage,
77    ) -> Result<(usize, Frame<String>, UpdateRegion)> {
78        let (w, h) = image.dimensions();
79        let (page_id, frame) = self.session.append(key, w, h)?;
80
81        // Ensure page exists
82        self.ensure_page(page_id);
83
84        // Blit image to page
85        let update_region = self.blit_to_page(page_id, &frame, image)?;
86
87        Ok((page_id, frame, update_region))
88    }
89
90    /// Append a texture by dimensions only (no pixel data).
91    /// Returns (page_id, frame).
92    pub fn append(&mut self, key: String, w: u32, h: u32) -> Result<(usize, Frame<String>)> {
93        self.session.append(key, w, h)
94    }
95
96    /// Evict a texture and optionally clear its region.
97    /// Returns the region that was cleared (if clear=true).
98    pub fn evict_with_clear(
99        &mut self,
100        page_id: usize,
101        key: &str,
102        clear: bool,
103    ) -> Option<UpdateRegion> {
104        // Get reserved slot before evicting (covers padding/extrude)
105        let slot_region = if clear {
106            self.session
107                .get_reserved_slot(key)
108                .map(|(pid, slot)| UpdateRegion {
109                    page_id: pid,
110                    x: slot.x,
111                    y: slot.y,
112                    width: slot.w,
113                    height: slot.h,
114                })
115        } else {
116            None
117        };
118
119        // Evict from session
120        if self.session.evict(page_id, key) {
121            // Clear pixels if requested
122            if clear && let Some(region) = slot_region {
123                self.clear_region(region);
124                return Some(region);
125            }
126            Some(UpdateRegion::empty())
127        } else {
128            None
129        }
130    }
131
132    /// Evict a texture by key and optionally clear its region.
133    pub fn evict_by_key_with_clear(&mut self, key: &str, clear: bool) -> Option<UpdateRegion> {
134        // Get reserved slot before evicting
135        let slot_region = if clear {
136            self.session
137                .get_reserved_slot(key)
138                .map(|(page_id, slot)| UpdateRegion {
139                    page_id,
140                    x: slot.x,
141                    y: slot.y,
142                    width: slot.w,
143                    height: slot.h,
144                })
145        } else {
146            None
147        };
148
149        if self.session.evict_by_key(key) {
150            if clear && let Some(region) = slot_region {
151                self.clear_region(region);
152                return Some(region);
153            }
154            Some(UpdateRegion::empty())
155        } else {
156            None
157        }
158    }
159
160    /// Get a reference to the pixel data of a page.
161    pub fn get_page_image(&self, page_id: usize) -> Option<&RgbaImage> {
162        self.pages.get(page_id)
163    }
164
165    /// Get a mutable reference to the pixel data of a page.
166    pub fn get_page_image_mut(&mut self, page_id: usize) -> Option<&mut RgbaImage> {
167        self.pages.get_mut(page_id)
168    }
169
170    /// Get the number of pages with pixel data.
171    pub fn num_pages(&self) -> usize {
172        self.pages.len()
173    }
174
175    // Delegate query methods to session
176    pub fn get_frame(&self, key: &str) -> Option<(usize, &Frame<String>)> {
177        self.session.get_frame(key)
178    }
179
180    pub fn contains(&self, key: &str) -> bool {
181        self.session.contains(key)
182    }
183
184    pub fn keys(&self) -> Vec<&str> {
185        self.session.keys()
186    }
187
188    pub fn texture_count(&self) -> usize {
189        self.session.texture_count()
190    }
191
192    pub fn stats(&self) -> RuntimeStats {
193        self.session.stats()
194    }
195
196    pub fn snapshot_atlas(&self) -> crate::model::Atlas<String> {
197        self.session.snapshot_atlas()
198    }
199
200    /// Ensure a page exists, creating it if necessary.
201    fn ensure_page(&mut self, page_id: usize) {
202        while self.pages.len() <= page_id {
203            let page_img = RgbaImage::from_pixel(
204                self.session.cfg.max_width,
205                self.session.cfg.max_height,
206                self.background_color,
207            );
208            self.pages.push(page_img);
209        }
210    }
211
212    /// Blit an image to a page at the frame's position.
213    fn blit_to_page(
214        &mut self,
215        page_id: usize,
216        frame: &Frame<String>,
217        image: &RgbaImage,
218    ) -> Result<UpdateRegion> {
219        let page = self
220            .pages
221            .get_mut(page_id)
222            .ok_or_else(|| TexPackerError::InvalidConfig("Page not found".into()))?;
223
224        let (src_w, src_h) = image.dimensions();
225        let dst_x = frame.frame.x;
226        let dst_y = frame.frame.y;
227
228        // Reuse core compositing (with extrusion and optional outlines)
229        let extrude = self.session.cfg.texture_extrusion;
230        let outlines = self.session.cfg.texture_outlines;
231        let dst = crate::compositing::BlitRect::new(dst_x, dst_y, frame.frame.w, frame.frame.h);
232        let src = crate::compositing::BlitRect::new(0, 0, src_w, src_h);
233        let options = crate::compositing::BlitOptions {
234            rotated: frame.rotated,
235            extrude,
236            outlines,
237        };
238        crate::compositing::blit_rgba(image, page, dst, src, options);
239
240        // Return the minimal update region including extrusion
241        let start_x = dst_x.saturating_sub(extrude);
242        let start_y = dst_y.saturating_sub(extrude);
243        let mut width = frame.frame.w + extrude.saturating_mul(2);
244        let mut height = frame.frame.h + extrude.saturating_mul(2);
245        // Clamp to page bounds
246        if start_x + width > page.width() {
247            width = page.width() - start_x;
248        }
249        if start_y + height > page.height() {
250            height = page.height() - start_y;
251        }
252
253        Ok(UpdateRegion {
254            page_id,
255            x: start_x,
256            y: start_y,
257            width,
258            height,
259        })
260    }
261
262    /// Clear a region on a page.
263    fn clear_region(&mut self, region: UpdateRegion) {
264        if let Some(page) = self.pages.get_mut(region.page_id) {
265            for y in region.y..(region.y + region.height).min(page.height()) {
266                for x in region.x..(region.x + region.width).min(page.width()) {
267                    page.put_pixel(x, y, self.background_color);
268                }
269            }
270        }
271    }
272}