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