tex_packer_core/
model.rs

1use serde::{Deserialize, Serialize};
2
3/// Axis-aligned rectangle (pixels). `x,y` is top-left; `w,h` are sizes.
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5pub struct Rect {
6    pub x: u32,
7    pub y: u32,
8    pub w: u32,
9    pub h: u32,
10}
11
12impl Rect {
13    pub fn new(x: u32, y: u32, w: u32, h: u32) -> Self {
14        Self { x, y, w, h }
15    }
16    /// Inclusive right edge coordinate (`x + w - 1`).
17    pub fn right(&self) -> u32 {
18        self.x + self.w.saturating_sub(1)
19    }
20    /// Inclusive bottom edge coordinate (`y + h - 1`).
21    pub fn bottom(&self) -> u32 {
22        self.y + self.h.saturating_sub(1)
23    }
24    /// Returns true if `r` is fully inside `self` (inclusive edges).
25    pub fn contains(&self, r: &Rect) -> bool {
26        r.x >= self.x && r.y >= self.y && r.right() <= self.right() && r.bottom() <= self.bottom()
27    }
28}
29
30/// A placed frame within a page.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Frame<K = String> {
33    /// User-specified key (e.g., filename or asset path).
34    pub key: K,
35    /// Placed rectangle within the page (post-rotation width/height).
36    pub frame: Rect,
37    /// True if the frame was rotated 90° when placed.
38    pub rotated: bool,
39    /// True if the source was trimmed.
40    pub trimmed: bool,
41    /// Source sub-rect within the original image after trimming.
42    pub source: Rect,
43    /// Original (untrimmed) image size.
44    pub source_size: (u32, u32),
45}
46
47/// A single atlas page (logical record).
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Page<K = String> {
50    pub id: usize,
51    pub width: u32,
52    pub height: u32,
53    pub frames: Vec<Frame<K>>,
54}
55
56/// Atlas-level metadata (common fields used by exporters/templates).
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Meta {
59    /// Schema version for JSON metadata formats (e.g., json-array/json-hash).
60    /// Allows downstream tooling to handle future additive changes.
61    /// String to allow non-integer versions like "1.0"; current: "1".
62    pub schema_version: String,
63    pub app: String,
64    pub version: String,
65    pub format: String,
66    pub scale: f32,
67    pub power_of_two: bool,
68    pub square: bool,
69    pub max_dim: (u32, u32),
70    pub padding: (u32, u32),
71    pub extrude: u32,
72    pub allow_rotation: bool,
73    pub trim_mode: String,
74    pub background_color: Option<[u8; 4]>,
75}
76
77/// Atlas of pages and metadata.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct Atlas<K = String> {
80    pub pages: Vec<Page<K>>,
81    pub meta: Meta,
82}
83
84/// Statistics about atlas packing efficiency.
85#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
86pub struct PackStats {
87    /// Total number of pages in the atlas.
88    pub num_pages: usize,
89    /// Total number of frames (textures) packed.
90    pub num_frames: usize,
91    /// Total area of all pages (sum of width * height for each page).
92    pub total_page_area: u64,
93    /// Total area used by all frames (sum of frame width * height).
94    pub used_frame_area: u64,
95    /// Occupancy ratio: used_frame_area / total_page_area (0.0 to 1.0).
96    /// Higher is better (less wasted space).
97    pub occupancy: f64,
98    /// Average page dimensions.
99    pub avg_page_width: f64,
100    pub avg_page_height: f64,
101    /// Largest page dimensions.
102    pub max_page_width: u32,
103    pub max_page_height: u32,
104    /// Number of rotated frames.
105    pub num_rotated: usize,
106    /// Number of trimmed frames.
107    pub num_trimmed: usize,
108}
109
110impl<K> Atlas<K> {
111    /// Computes packing statistics for this atlas.
112    pub fn stats(&self) -> PackStats {
113        let num_pages = self.pages.len();
114        let mut num_frames = 0;
115        let mut total_page_area = 0u64;
116        let mut used_frame_area = 0u64;
117        let mut max_page_width = 0u32;
118        let mut max_page_height = 0u32;
119        let mut num_rotated = 0;
120        let mut num_trimmed = 0;
121
122        for page in &self.pages {
123            let page_area = (page.width as u64) * (page.height as u64);
124            total_page_area += page_area;
125            max_page_width = max_page_width.max(page.width);
126            max_page_height = max_page_height.max(page.height);
127
128            for frame in &page.frames {
129                num_frames += 1;
130                let frame_area = (frame.frame.w as u64) * (frame.frame.h as u64);
131                used_frame_area += frame_area;
132
133                if frame.rotated {
134                    num_rotated += 1;
135                }
136                if frame.trimmed {
137                    num_trimmed += 1;
138                }
139            }
140        }
141
142        let occupancy = if total_page_area > 0 {
143            used_frame_area as f64 / total_page_area as f64
144        } else {
145            0.0
146        };
147
148        let (avg_page_width, avg_page_height) = if num_pages > 0 {
149            let total_width: u64 = self.pages.iter().map(|p| p.width as u64).sum();
150            let total_height: u64 = self.pages.iter().map(|p| p.height as u64).sum();
151            (
152                total_width as f64 / num_pages as f64,
153                total_height as f64 / num_pages as f64,
154            )
155        } else {
156            (0.0, 0.0)
157        };
158
159        PackStats {
160            num_pages,
161            num_frames,
162            total_page_area,
163            used_frame_area,
164            occupancy,
165            avg_page_width,
166            avg_page_height,
167            max_page_width,
168            max_page_height,
169            num_rotated,
170            num_trimmed,
171        }
172    }
173}
174
175impl PackStats {
176    /// Returns a human-readable summary of the statistics.
177    pub fn summary(&self) -> String {
178        format!(
179            "Pages: {}, Frames: {}, Occupancy: {:.2}%, Total Area: {} px², Used Area: {} px², Rotated: {}, Trimmed: {}",
180            self.num_pages,
181            self.num_frames,
182            self.occupancy * 100.0,
183            self.total_page_area,
184            self.used_frame_area,
185            self.num_rotated,
186            self.num_trimmed,
187        )
188    }
189
190    /// Returns wasted space in pixels.
191    pub fn wasted_area(&self) -> u64 {
192        self.total_page_area.saturating_sub(self.used_frame_area)
193    }
194
195    /// Returns wasted space as a percentage (0.0 to 100.0).
196    pub fn waste_percentage(&self) -> f64 {
197        if self.total_page_area > 0 {
198            (self.wasted_area() as f64 / self.total_page_area as f64) * 100.0
199        } else {
200            0.0
201        }
202    }
203}