1use std::collections::HashMap;
2
3use image::{Rgba, RgbaImage};
4
5use crate::config::RuntimeConfig;
6use crate::error::{Result, TexPackerError};
7use crate::model::{Atlas, PageId, Rect};
8use crate::runtime::{AtlasSession, PreparedAppend, RuntimePlacement, RuntimeStats};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct UpdateRegion {
13 pub page_id: PageId,
14 pub x: u32,
15 pub y: u32,
16 pub width: u32,
17 pub height: u32,
18}
19
20impl UpdateRegion {
21 pub const fn empty() -> Self {
22 Self {
23 page_id: PageId::new(0),
24 x: 0,
25 y: 0,
26 width: 0,
27 height: 0,
28 }
29 }
30
31 pub const fn is_empty(&self) -> bool {
32 self.width == 0 || self.height == 0
33 }
34
35 pub const fn area(&self) -> u64 {
36 self.width as u64 * self.height as u64
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct RuntimeImageUpdate {
43 placement: RuntimePlacement,
44 dirty_region: UpdateRegion,
45}
46
47impl RuntimeImageUpdate {
48 pub const fn placement(&self) -> &RuntimePlacement {
49 &self.placement
50 }
51
52 pub const fn dirty_region(&self) -> UpdateRegion {
53 self.dirty_region
54 }
55}
56
57struct RuntimeImagePage {
58 id: PageId,
59 image: RgbaImage,
60}
61
62enum PreparedPixels {
63 Patch {
64 page_index: usize,
65 region: UpdateRegion,
66 pixels: RgbaImage,
67 },
68 New(RuntimeImagePage),
69}
70
71pub struct RuntimeAtlas {
73 session: AtlasSession,
74 pages: Vec<RuntimeImagePage>,
75 page_slots: HashMap<PageId, usize>,
76 background_color: Rgba<u8>,
77 outlines: bool,
78}
79
80impl RuntimeAtlas {
81 pub fn new(cfg: RuntimeConfig) -> Self {
82 Self {
83 session: AtlasSession::new(cfg),
84 pages: Vec::new(),
85 page_slots: HashMap::new(),
86 background_color: Rgba([0, 0, 0, 0]),
87 outlines: false,
88 }
89 }
90
91 pub fn with_background_color(mut self, color: Rgba<u8>) -> Self {
92 self.background_color = color;
93 self
94 }
95
96 pub fn with_outlines(mut self, enabled: bool) -> Self {
97 self.outlines = enabled;
98 self
99 }
100
101 pub fn append_with_image(
102 &mut self,
103 key: String,
104 image: &RgbaImage,
105 ) -> Result<RuntimeImageUpdate> {
106 let (width, height) = image.dimensions();
107 let prepared_append = self.session.prepare_append(key, width, height)?;
108 let (prepared_pixels, dirty_region) = self.prepare_pixels(&prepared_append, image)?;
109 let placement = self.commit_image_append(prepared_append, prepared_pixels);
110
111 Ok(RuntimeImageUpdate {
112 placement,
113 dirty_region,
114 })
115 }
116
117 pub fn append(&mut self, key: String, w: u32, h: u32) -> Result<RuntimePlacement> {
118 self.session.append(key, w, h)
119 }
120
121 pub fn evict_with_clear(
122 &mut self,
123 page_id: PageId,
124 key: &str,
125 clear: bool,
126 ) -> Option<UpdateRegion> {
127 let allocation = clear.then(|| self.session.get_reserved_slot(key)).flatten();
128
129 if !self.session.evict(page_id, key) {
130 return None;
131 }
132
133 if let Some((allocation_page_id, rect)) = allocation {
134 let region = update_region(allocation_page_id, rect);
135 self.clear_region(region);
136 Some(region)
137 } else {
138 Some(UpdateRegion::empty())
139 }
140 }
141
142 pub fn evict_by_key_with_clear(&mut self, key: &str, clear: bool) -> Option<UpdateRegion> {
143 let allocation = clear.then(|| self.session.get_reserved_slot(key)).flatten();
144
145 if !self.session.evict_by_key(key) {
146 return None;
147 }
148
149 if let Some((page_id, rect)) = allocation {
150 let region = update_region(page_id, rect);
151 self.clear_region(region);
152 Some(region)
153 } else {
154 Some(UpdateRegion::empty())
155 }
156 }
157
158 pub fn get_page_image(&self, page_id: PageId) -> Option<&RgbaImage> {
159 let page_slot = *self.page_slots.get(&page_id)?;
160 self.pages.get(page_slot).map(|page| &page.image)
161 }
162
163 pub fn get_page_image_mut(&mut self, page_id: PageId) -> Option<&mut RgbaImage> {
164 let page_slot = *self.page_slots.get(&page_id)?;
165 self.pages.get_mut(page_slot).map(|page| &mut page.image)
166 }
167
168 pub fn num_pages(&self) -> usize {
169 self.pages.len()
170 }
171
172 pub fn get_frame(&self, key: &str) -> Option<RuntimePlacement> {
173 self.session.get_frame(key)
174 }
175
176 pub fn contains(&self, key: &str) -> bool {
177 self.session.contains(key)
178 }
179
180 pub fn keys(&self) -> Vec<&str> {
181 self.session.keys()
182 }
183
184 pub fn texture_count(&self) -> usize {
185 self.session.texture_count()
186 }
187
188 pub fn stats(&self) -> RuntimeStats {
189 self.session.stats()
190 }
191
192 pub fn snapshot_atlas(&self) -> Result<Atlas> {
193 self.session.snapshot_atlas()
194 }
195
196 fn prepare_pixels(
197 &self,
198 prepared_append: &PreparedAppend,
199 image: &RgbaImage,
200 ) -> Result<(PreparedPixels, UpdateRegion)> {
201 let placement = prepared_append.placement();
202 let page_id = placement.page_id();
203 let extrusion = self.session.cfg.page_config().texture_extrusion();
204 let page_size = prepared_append.page_size();
205 let dirty_region = validate_blit(placement, image, page_size, extrusion)?;
206
207 if let Some(&page_index) = self.page_slots.get(&page_id) {
208 let page =
209 self.pages
210 .get(page_index)
211 .ok_or_else(|| TexPackerError::InvariantViolation {
212 context: format!("runtime image page {page_id}"),
213 reason: format!("page index references missing slot {page_index}"),
214 })?;
215 if page.image.dimensions() != page_size {
216 return Err(TexPackerError::InvariantViolation {
217 context: format!("runtime image page {page_id}"),
218 reason: format!(
219 "pixel buffer dimensions {:?} do not match geometry page {:?}",
220 page.image.dimensions(),
221 page_size
222 ),
223 });
224 }
225
226 let mut pixels = RgbaImage::from_pixel(
227 dirty_region.width,
228 dirty_region.height,
229 self.background_color,
230 );
231 let content = placement.content();
232 blit_staged(
233 image,
234 &mut pixels,
235 Rect::new(
236 content.x - dirty_region.x,
237 content.y - dirty_region.y,
238 content.w,
239 content.h,
240 ),
241 placement.rotated(),
242 extrusion,
243 self.outlines,
244 );
245 Ok((
246 PreparedPixels::Patch {
247 page_index,
248 region: dirty_region,
249 pixels,
250 },
251 dirty_region,
252 ))
253 } else {
254 let mut pixels = RgbaImage::from_pixel(page_size.0, page_size.1, self.background_color);
255 blit_staged(
256 image,
257 &mut pixels,
258 placement.content(),
259 placement.rotated(),
260 extrusion,
261 self.outlines,
262 );
263 Ok((
264 PreparedPixels::New(RuntimeImagePage {
265 id: page_id,
266 image: pixels,
267 }),
268 dirty_region,
269 ))
270 }
271 }
272
273 fn commit_pixels(&mut self, prepared: PreparedPixels) {
274 match prepared {
275 PreparedPixels::Patch {
276 page_index,
277 region,
278 pixels,
279 } => {
280 let page = &mut self.pages[page_index].image;
281 for (x, y, pixel) in pixels.enumerate_pixels() {
282 page.put_pixel(region.x + x, region.y + y, *pixel);
283 }
284 }
285 PreparedPixels::New(page) => {
286 let page_id = page.id;
287 let page_slot = self.pages.len();
288 self.pages.push(page);
289 let replaced = self.page_slots.insert(page_id, page_slot);
290 debug_assert!(replaced.is_none(), "new pixel page identity must be unique");
291 }
292 }
293 }
294
295 fn commit_image_append(
296 &mut self,
297 prepared_append: PreparedAppend,
298 prepared_pixels: PreparedPixels,
299 ) -> RuntimePlacement {
300 let placement = self.session.commit_append(prepared_append);
301 self.commit_pixels(prepared_pixels);
302 placement
303 }
304
305 fn clear_region(&mut self, region: UpdateRegion) {
306 let background_color = self.background_color;
307 let Some(page) = self.get_page_image_mut(region.page_id) else {
308 return;
309 };
310
311 for y in region.y..region.y.saturating_add(region.height).min(page.height()) {
312 for x in region.x..region.x.saturating_add(region.width).min(page.width()) {
313 page.put_pixel(x, y, background_color);
314 }
315 }
316 }
317}
318
319fn validate_blit(
320 placement: &RuntimePlacement,
321 image: &RgbaImage,
322 page_size: (u32, u32),
323 extrusion: u32,
324) -> Result<UpdateRegion> {
325 let page_id = placement.page_id();
326 let content = placement.content();
327 let source_size = image.dimensions();
328 let expected_content_size = if placement.rotated() {
329 (source_size.1, source_size.0)
330 } else {
331 source_size
332 };
333 if content.is_empty() || (content.w, content.h) != expected_content_size {
334 return Err(blit_invariant(
335 page_id,
336 format!(
337 "content dimensions {:?} do not match source {:?} with rotated={}",
338 (content.w, content.h),
339 source_size,
340 placement.rotated()
341 ),
342 ));
343 }
344
345 let Some(start_x) = content.x.checked_sub(extrusion) else {
346 return Err(blit_invariant(page_id, "extrusion crosses the left edge"));
347 };
348 let Some(start_y) = content.y.checked_sub(extrusion) else {
349 return Err(blit_invariant(page_id, "extrusion crosses the top edge"));
350 };
351 let end_x = u64::from(content.x) + u64::from(content.w) + u64::from(extrusion);
352 let end_y = u64::from(content.y) + u64::from(content.h) + u64::from(extrusion);
353 if end_x > u64::from(page_size.0) || end_y > u64::from(page_size.1) {
354 return Err(blit_invariant(
355 page_id,
356 format!(
357 "destination including extrusion exceeds page dimensions {:?}",
358 page_size
359 ),
360 ));
361 }
362
363 let region = UpdateRegion {
364 page_id,
365 x: start_x,
366 y: start_y,
367 width: u32::try_from(end_x - u64::from(start_x))
368 .map_err(|_| blit_invariant(page_id, "dirty width exceeds u32"))?,
369 height: u32::try_from(end_y - u64::from(start_y))
370 .map_err(|_| blit_invariant(page_id, "dirty height exceeds u32"))?,
371 };
372 let dirty_rect = Rect::new(region.x, region.y, region.width, region.height);
373 if !placement.allocation().contains(&dirty_rect) {
374 return Err(blit_invariant(
375 page_id,
376 "dirty rectangle must lie inside the reserved allocation",
377 ));
378 }
379 Ok(region)
380}
381
382fn blit_staged(
383 image: &RgbaImage,
384 pixels: &mut RgbaImage,
385 content: Rect,
386 rotated: bool,
387 extrusion: u32,
388 outlines: bool,
389) {
390 let (source_width, source_height) = image.dimensions();
391 crate::compositing::blit_rgba(
392 image,
393 pixels,
394 crate::compositing::BlitRect::new(content.x, content.y, content.w, content.h),
395 crate::compositing::BlitRect::new(0, 0, source_width, source_height),
396 crate::compositing::BlitOptions {
397 rotated,
398 extrude: extrusion,
399 outlines,
400 },
401 );
402}
403
404fn blit_invariant(page_id: PageId, reason: impl Into<String>) -> TexPackerError {
405 TexPackerError::InvariantViolation {
406 context: format!("runtime image page {page_id}"),
407 reason: reason.into(),
408 }
409}
410
411fn update_region(page_id: PageId, rect: Rect) -> UpdateRegion {
412 UpdateRegion {
413 page_id,
414 x: rect.x,
415 y: rect.y,
416 width: rect.w,
417 height: rect.h,
418 }
419}