Skip to main content

tex_packer_core/
runtime.rs

1use crate::config::{PackerConfig, SkylineHeuristic};
2use crate::error::{Result, TexPackerError};
3use crate::geometry::PlacementGeometry;
4use crate::model::{Atlas, Frame, Meta, Page, Rect};
5use crate::runtime_placement::RuntimePage;
6
7#[derive(Debug, Clone)]
8pub enum RuntimeStrategy {
9    Guillotine,
10    Shelf(ShelfPolicy),
11    Skyline(SkylineHeuristic),
12}
13
14#[derive(Debug, Clone, Copy)]
15pub enum ShelfPolicy {
16    NextFit,
17    FirstFit,
18}
19
20/// Runtime statistics for an atlas session.
21#[derive(Debug, Clone)]
22pub struct RuntimeStats {
23    /// Number of pages currently allocated.
24    pub num_pages: usize,
25    /// Number of textures currently in the atlas.
26    pub num_textures: usize,
27    /// Total area of all pages (width * height * num_pages).
28    pub total_page_area: u64,
29    /// Total area occupied by textures (sum of all used slots).
30    pub total_used_area: u64,
31    /// Total free area available for new textures.
32    pub total_free_area: u64,
33    /// Occupancy ratio: used_area / total_page_area (0.0 to 1.0).
34    pub occupancy: f64,
35    /// Number of free rectangles/segments (fragmentation indicator).
36    pub num_free_rects: usize,
37}
38
39impl RuntimeStats {
40    /// Returns a human-readable summary of the statistics.
41    pub fn summary(&self) -> String {
42        format!(
43            "Pages: {}, Textures: {}, Occupancy: {:.2}%, Free: {} px² ({} rects), Used: {} px²",
44            self.num_pages,
45            self.num_textures,
46            self.occupancy * 100.0,
47            self.total_free_area,
48            self.num_free_rects,
49            self.total_used_area,
50        )
51    }
52
53    /// Returns the fragmentation ratio (higher = more fragmented).
54    /// Calculated as: num_free_rects / (total_free_area / 1000)
55    /// Lower is better (fewer, larger free blocks).
56    pub fn fragmentation(&self) -> f64 {
57        if self.total_free_area == 0 {
58            0.0
59        } else {
60            (self.num_free_rects as f64) / ((self.total_free_area as f64) / 1000.0).max(1.0)
61        }
62    }
63
64    /// Returns the waste percentage (0.0 to 100.0).
65    pub fn waste_percentage(&self) -> f64 {
66        if self.total_page_area > 0 {
67            (self.total_free_area as f64 / self.total_page_area as f64) * 100.0
68        } else {
69            0.0
70        }
71    }
72}
73
74pub struct AtlasSession {
75    pub(crate) cfg: PackerConfig,
76    _strategy: RuntimeStrategy,
77    pages: Vec<RuntimePage>,
78    next_id: usize,
79}
80
81impl AtlasSession {
82    pub fn new(cfg: PackerConfig, strategy: RuntimeStrategy) -> Self {
83        Self {
84            cfg,
85            _strategy: strategy,
86            pages: Vec::new(),
87            next_id: 0,
88        }
89    }
90
91    fn new_page(&mut self) -> RuntimePage {
92        let id = self.next_id;
93        self.next_id += 1;
94        RuntimePage::new(
95            id,
96            self.cfg.max_width,
97            self.cfg.max_height,
98            &self.cfg,
99            &self._strategy,
100        )
101    }
102
103    pub fn append(&mut self, key: String, w: u32, h: u32) -> Result<(usize, Frame<String>)> {
104        let geometry = PlacementGeometry::from_size(w, h, &self.cfg);
105        // Try existing pages
106        for idx in 0..self.pages.len() {
107            let (slot, rotated, id);
108            {
109                let p = &self.pages[idx];
110                if let Some((s, r)) = p.choose(geometry.reserved_w, geometry.reserved_h) {
111                    slot = s;
112                    rotated = r;
113                    id = p.id;
114                } else {
115                    continue;
116                }
117            }
118            let frame = geometry.frame(key.clone(), Rect::new(0, 0, w, h), &slot, rotated);
119            let p = &mut self.pages[idx];
120            p.place(&key, &slot, &frame, rotated);
121            return Ok((id, frame));
122        }
123        // Grow: add a new page and place
124        let mut page = self.new_page();
125        if let Some((slot, rotated)) = page.choose(geometry.reserved_w, geometry.reserved_h) {
126            let frame = geometry.frame(key.clone(), Rect::new(0, 0, w, h), &slot, rotated);
127            page.place(&key, &slot, &frame, rotated);
128            let id = page.id;
129            self.pages.push(page);
130            return Ok((id, frame));
131        }
132        Err(TexPackerError::OutOfSpace {
133            key,
134            width: w,
135            height: h,
136            pages_attempted: self.pages.len() + 1,
137        })
138    }
139
140    pub fn evict(&mut self, page_id: usize, key: &str) -> bool {
141        if let Some(p) = self.pages.iter_mut().find(|p| p.id == page_id)
142            && let Some((slot, _rot, _frame)) = p.used.remove(key)
143        {
144            p.add_free(slot);
145            return true;
146        }
147        false
148    }
149
150    pub fn snapshot_atlas(&self) -> Atlas<String> {
151        let mut pages: Vec<Page<String>> = Vec::new();
152        for p in &self.pages {
153            let mut frames: Vec<Frame<String>> = Vec::new();
154            for (_k, (_slot, _rot, f)) in p.used.iter() {
155                frames.push(f.clone());
156            }
157            pages.push(Page {
158                id: p.id,
159                width: p.width,
160                height: p.height,
161                frames,
162            });
163        }
164        let meta = Meta {
165            schema_version: "1".into(),
166            app: "tex-packer".into(),
167            version: env!("CARGO_PKG_VERSION").into(),
168            format: "RGBA8888".into(),
169            scale: 1.0,
170            power_of_two: self.cfg.power_of_two,
171            square: self.cfg.square,
172            max_dim: (self.cfg.max_width, self.cfg.max_height),
173            padding: (self.cfg.border_padding, self.cfg.texture_padding),
174            extrude: self.cfg.texture_extrusion,
175            allow_rotation: self.cfg.allow_rotation,
176            trim_mode: if self.cfg.trim { "trim" } else { "none" }.into(),
177            background_color: None,
178        };
179        Atlas { pages, meta }
180    }
181
182    /// Find a frame by its key.
183    /// Returns the page ID and a reference to the frame if found.
184    pub fn get_frame(&self, key: &str) -> Option<(usize, &Frame<String>)> {
185        for page in &self.pages {
186            if let Some((_slot, _rot, frame)) = page.used.get(key) {
187                return Some((page.id, frame));
188            }
189        }
190        None
191    }
192
193    /// Find the reserved slot rectangle for a texture key (pre-offset area including padding/extrude).
194    pub fn get_reserved_slot(&self, key: &str) -> Option<(usize, Rect)> {
195        for page in &self.pages {
196            if let Some((slot, _rot, _frame)) = page.used.get(key) {
197                return Some((page.id, *slot));
198            }
199        }
200        None
201    }
202
203    /// Evict a texture by its key without needing to know the page ID.
204    /// Returns true if the texture was found and evicted.
205    pub fn evict_by_key(&mut self, key: &str) -> bool {
206        for page in &mut self.pages {
207            if let Some((slot, _rot, _frame)) = page.used.remove(key) {
208                page.add_free(slot);
209                return true;
210            }
211        }
212        false
213    }
214
215    /// Check if a texture with the given key exists.
216    pub fn contains(&self, key: &str) -> bool {
217        self.pages.iter().any(|p| p.used.contains_key(key))
218    }
219
220    /// Get all texture keys currently in the atlas.
221    pub fn keys(&self) -> Vec<&str> {
222        self.pages
223            .iter()
224            .flat_map(|p| p.used.keys().map(|k| k.as_str()))
225            .collect()
226    }
227
228    /// Get the number of textures currently in the atlas.
229    pub fn texture_count(&self) -> usize {
230        self.pages.iter().map(|p| p.used.len()).sum()
231    }
232
233    /// Get runtime statistics about the atlas session.
234    pub fn stats(&self) -> RuntimeStats {
235        let num_pages = self.pages.len();
236        let num_textures = self.texture_count();
237
238        let mut total_used_area = 0u64;
239        let mut total_free_area = 0u64;
240        let mut num_free_rects = 0;
241
242        for page in &self.pages {
243            total_used_area += page.used_area();
244            let (free_area, free_rects) = page.free_area_and_rects();
245            total_free_area += free_area;
246            num_free_rects += free_rects;
247        }
248
249        let total_page_area = if num_pages > 0 {
250            (self.cfg.max_width as u64) * (self.cfg.max_height as u64) * (num_pages as u64)
251        } else {
252            0
253        };
254
255        let occupancy = if total_page_area > 0 {
256            total_used_area as f64 / total_page_area as f64
257        } else {
258            0.0
259        };
260
261        RuntimeStats {
262            num_pages,
263            num_textures,
264            total_page_area,
265            total_used_area,
266            total_free_area,
267            occupancy,
268            num_free_rects,
269        }
270    }
271}