1use std::collections::{BTreeMap, HashMap};
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6use crate::config::PageConfig;
7use crate::error::{Result, TexPackerError};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct Rect {
12 pub x: u32,
13 pub y: u32,
14 pub w: u32,
15 pub h: u32,
16}
17
18impl Rect {
19 pub const fn new(x: u32, y: u32, w: u32, h: u32) -> Self {
20 Self { x, y, w, h }
21 }
22
23 pub const fn right(self) -> u32 {
25 self.x.saturating_add(self.w.saturating_sub(1))
26 }
27
28 pub const fn bottom(self) -> u32 {
30 self.y.saturating_add(self.h.saturating_sub(1))
31 }
32
33 pub const fn is_empty(self) -> bool {
34 self.w == 0 || self.h == 0
35 }
36
37 pub const fn area(self) -> u128 {
38 self.w as u128 * self.h as u128
39 }
40
41 pub fn contains(self, inner: &Rect) -> bool {
43 !self.is_empty()
44 && !inner.is_empty()
45 && inner.x >= self.x
46 && inner.y >= self.y
47 && inner.right_exclusive() <= self.right_exclusive()
48 && inner.bottom_exclusive() <= self.bottom_exclusive()
49 }
50
51 pub fn intersects(self, other: &Rect) -> bool {
53 !self.is_empty()
54 && !other.is_empty()
55 && (self.x as u64) < other.right_exclusive()
56 && (other.x as u64) < self.right_exclusive()
57 && (self.y as u64) < other.bottom_exclusive()
58 && (other.y as u64) < self.bottom_exclusive()
59 }
60
61 const fn right_exclusive(self) -> u64 {
62 self.x as u64 + self.w as u64
63 }
64
65 const fn bottom_exclusive(self) -> u64 {
66 self.y as u64 + self.h as u64
67 }
68}
69
70macro_rules! identity_type {
71 ($(#[$meta:meta])* $name:ident) => {
72 $(#[$meta])*
73 #[repr(transparent)]
74 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
75 pub struct $name(u32);
76
77 impl $name {
78 pub const fn new(value: u32) -> Self {
79 Self(value)
80 }
81
82 pub const fn get(self) -> u32 {
83 self.0
84 }
85 }
86
87 impl fmt::Display for $name {
88 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 self.0.fmt(formatter)
90 }
91 }
92 };
93}
94
95identity_type!(
96 PageId
98);
99identity_type!(
100 RegionId
102);
103identity_type!(
104 FrameId
106);
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct Region {
111 id: RegionId,
112 content: Rect,
113 allocation: Rect,
114 rotated: bool,
115}
116
117impl Region {
118 pub const fn new(id: RegionId, content: Rect, allocation: Rect, rotated: bool) -> Self {
119 Self {
120 id,
121 content,
122 allocation,
123 rotated,
124 }
125 }
126
127 pub const fn id(&self) -> RegionId {
128 self.id
129 }
130
131 pub const fn content(&self) -> Rect {
132 self.content
133 }
134
135 pub const fn allocation(&self) -> Rect {
136 self.allocation
137 }
138
139 pub const fn rotated(&self) -> bool {
140 self.rotated
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct Frame {
147 id: FrameId,
148 key: String,
149 region_id: RegionId,
150 trimmed: bool,
151 source: Rect,
152 source_size: (u32, u32),
153}
154
155impl Frame {
156 pub fn new(
157 id: FrameId,
158 key: String,
159 region_id: RegionId,
160 trimmed: bool,
161 source: Rect,
162 source_size: (u32, u32),
163 ) -> Self {
164 Self {
165 id,
166 key,
167 region_id,
168 trimmed,
169 source,
170 source_size,
171 }
172 }
173
174 pub const fn id(&self) -> FrameId {
175 self.id
176 }
177
178 pub fn key(&self) -> &str {
179 &self.key
180 }
181
182 pub const fn region_id(&self) -> RegionId {
183 self.region_id
184 }
185
186 pub const fn trimmed(&self) -> bool {
187 self.trimmed
188 }
189
190 pub const fn source(&self) -> Rect {
191 self.source
192 }
193
194 pub const fn source_size(&self) -> (u32, u32) {
195 self.source_size
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200struct PageIndex {
201 region_by_id: HashMap<RegionId, usize>,
202 frame_by_id: HashMap<FrameId, usize>,
203 frame_region_slots: Vec<usize>,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct Page {
209 id: PageId,
210 width: u32,
211 height: u32,
212 regions: Vec<Region>,
213 frames: Vec<Frame>,
214 index: PageIndex,
215}
216
217impl Page {
218 pub fn try_new(
219 id: PageId,
220 width: u32,
221 height: u32,
222 regions: Vec<Region>,
223 frames: Vec<Frame>,
224 ) -> Result<Self> {
225 let page_context = format!("page {id}");
226 if width == 0 || height == 0 {
227 return Err(invariant(
228 page_context,
229 format!("page dimensions must be positive, got {width}x{height}"),
230 ));
231 }
232
233 let page_bounds = Rect::new(0, 0, width, height);
234 let mut region_by_id = HashMap::with_capacity(regions.len());
235 for (slot, region) in regions.iter().enumerate() {
236 let context = region_context(id, region.id);
237 if region_by_id.insert(region.id, slot).is_some() {
238 return Err(invariant(context, "duplicate region identity"));
239 }
240 if region.content.is_empty() {
241 return Err(invariant(context, "content rectangle must be non-empty"));
242 }
243 if region.allocation.is_empty() {
244 return Err(invariant(context, "allocation rectangle must be non-empty"));
245 }
246 if !region.allocation.contains(®ion.content) {
247 return Err(invariant(
248 context,
249 format!(
250 "content rectangle {:?} must lie inside allocation {:?}",
251 region.content, region.allocation
252 ),
253 ));
254 }
255 if !page_bounds.contains(®ion.allocation) {
256 return Err(invariant(
257 context,
258 format!(
259 "allocation rectangle {:?} must lie inside page bounds {width}x{height}",
260 region.allocation
261 ),
262 ));
263 }
264 }
265
266 if let Some((first, second)) = overlapping_allocations(®ions) {
267 return Err(invariant(
268 page_context.clone(),
269 format!("allocations for regions {first} and {second} overlap"),
270 ));
271 }
272
273 let mut frame_by_id = HashMap::with_capacity(frames.len());
274 let mut frame_region_slots = Vec::with_capacity(frames.len());
275 let mut region_references = vec![0usize; regions.len()];
276 for (slot, frame) in frames.iter().enumerate() {
277 let context = frame_context(id, frame);
278 if frame_by_id.insert(frame.id, slot).is_some() {
279 return Err(invariant(context, "duplicate frame identity"));
280 }
281
282 let Some(®ion_slot) = region_by_id.get(&frame.region_id) else {
283 return Err(invariant(
284 context,
285 format!("references missing region {}", frame.region_id),
286 ));
287 };
288 let region = ®ions[region_slot];
289
290 if frame.source.is_empty() {
291 return Err(invariant(context, "source rectangle must be non-empty"));
292 }
293 let source_bounds = Rect::new(0, 0, frame.source_size.0, frame.source_size.1);
294 if !source_bounds.contains(&frame.source) {
295 return Err(invariant(
296 context,
297 format!(
298 "source rectangle {:?} must lie inside source size {}x{}",
299 frame.source, frame.source_size.0, frame.source_size.1
300 ),
301 ));
302 }
303
304 let expected_content_size = if region.rotated {
305 (frame.source.h, frame.source.w)
306 } else {
307 (frame.source.w, frame.source.h)
308 };
309 if (region.content.w, region.content.h) != expected_content_size {
310 return Err(invariant(
311 context,
312 format!(
313 "source size {}x{} does not match region content {}x{} with rotated={}",
314 frame.source.w,
315 frame.source.h,
316 region.content.w,
317 region.content.h,
318 region.rotated
319 ),
320 ));
321 }
322
323 frame_region_slots.push(region_slot);
324 region_references[region_slot] += 1;
325 }
326
327 for (region, references) in regions.iter().zip(region_references) {
328 if references == 0 {
329 return Err(invariant(
330 region_context(id, region.id),
331 "physical region is not referenced by any frame",
332 ));
333 }
334 }
335
336 Ok(Self {
337 id,
338 width,
339 height,
340 regions,
341 frames,
342 index: PageIndex {
343 region_by_id,
344 frame_by_id,
345 frame_region_slots,
346 },
347 })
348 }
349
350 pub const fn id(&self) -> PageId {
351 self.id
352 }
353
354 pub const fn width(&self) -> u32 {
355 self.width
356 }
357
358 pub const fn height(&self) -> u32 {
359 self.height
360 }
361
362 pub const fn size(&self) -> (u32, u32) {
363 (self.width, self.height)
364 }
365
366 pub fn regions(&self) -> &[Region] {
367 &self.regions
368 }
369
370 pub fn frames(&self) -> &[Frame] {
371 &self.frames
372 }
373
374 pub fn region(&self, id: RegionId) -> Option<&Region> {
375 self.index
376 .region_by_id
377 .get(&id)
378 .and_then(|&slot| self.regions.get(slot))
379 }
380
381 pub fn frame(&self, id: FrameId) -> Option<&Frame> {
382 self.index
383 .frame_by_id
384 .get(&id)
385 .and_then(|&slot| self.frames.get(slot))
386 }
387
388 pub fn resolved_frames(
389 &self,
390 ) -> impl ExactSizeIterator<Item = ResolvedFrame<'_>> + DoubleEndedIterator + '_ {
391 self.frames
392 .iter()
393 .zip(self.index.frame_region_slots.iter().copied())
394 .map(move |(frame, region_slot)| ResolvedFrame {
395 page_id: self.id,
396 page_size: (self.width, self.height),
397 frame,
398 region: &self.regions[region_slot],
399 })
400 }
401}
402
403fn overlapping_allocations(regions: &[Region]) -> Option<(RegionId, RegionId)> {
404 let mut events = Vec::with_capacity(regions.len().saturating_mul(2));
405 for (slot, region) in regions.iter().enumerate() {
406 events.push((region.allocation.x as u64, true, slot));
407 events.push((region.allocation.right_exclusive(), false, slot));
408 }
409 events.sort_unstable_by_key(|&(x, starts, slot)| (x, starts, slot));
410
411 let mut active_by_y: BTreeMap<u32, usize> = BTreeMap::new();
412 for (_, starts, slot) in events {
413 let allocation = regions[slot].allocation;
414 if !starts {
415 active_by_y.remove(&allocation.y);
416 continue;
417 }
418
419 if let Some((_, &other_slot)) = active_by_y.range(..=allocation.y).next_back()
422 && allocation.intersects(®ions[other_slot].allocation)
423 {
424 return Some((regions[other_slot].id, regions[slot].id));
425 }
426 if let Some((_, &other_slot)) = active_by_y.range(allocation.y..).next()
427 && allocation.intersects(®ions[other_slot].allocation)
428 {
429 return Some((regions[other_slot].id, regions[slot].id));
430 }
431
432 let replaced = active_by_y.insert(allocation.y, slot);
433 debug_assert!(
434 replaced.is_none(),
435 "non-overlapping active allocations must have distinct y origins"
436 );
437 }
438 None
439}
440
441#[derive(Debug, Clone, Copy)]
443pub struct ResolvedFrame<'a> {
444 page_id: PageId,
445 page_size: (u32, u32),
446 frame: &'a Frame,
447 region: &'a Region,
448}
449
450impl<'a> ResolvedFrame<'a> {
451 pub const fn page_id(self) -> PageId {
452 self.page_id
453 }
454
455 pub const fn page_size(self) -> (u32, u32) {
456 self.page_size
457 }
458
459 pub const fn frame(self) -> &'a Frame {
460 self.frame
461 }
462
463 pub const fn region(self) -> &'a Region {
464 self.region
465 }
466}
467
468#[derive(Debug, Clone, PartialEq)]
470pub struct Meta {
471 app: String,
472 version: String,
473 format: String,
474 scale: f32,
475 power_of_two: bool,
476 square: bool,
477 max_dimensions: (u32, u32),
478 padding: (u32, u32),
479 extrude: u32,
480 allow_rotation: bool,
481 trim_mode: String,
482 background_color: Option<[u8; 4]>,
483}
484
485impl Meta {
486 pub fn app(&self) -> &str {
487 &self.app
488 }
489
490 pub fn version(&self) -> &str {
491 &self.version
492 }
493
494 pub fn format(&self) -> &str {
495 &self.format
496 }
497
498 pub const fn scale(&self) -> f32 {
499 self.scale
500 }
501
502 pub const fn power_of_two(&self) -> bool {
504 self.power_of_two
505 }
506
507 pub const fn square(&self) -> bool {
509 self.square
510 }
511
512 pub const fn max_dimensions(&self) -> (u32, u32) {
514 self.max_dimensions
515 }
516
517 pub const fn padding(&self) -> (u32, u32) {
519 self.padding
520 }
521
522 pub const fn extrude(&self) -> u32 {
523 self.extrude
524 }
525
526 pub const fn allow_rotation(&self) -> bool {
527 self.allow_rotation
528 }
529
530 pub fn trim_mode(&self) -> &str {
531 &self.trim_mode
532 }
533
534 pub const fn background_color(&self) -> Option<[u8; 4]> {
535 self.background_color
536 }
537
538 pub(crate) fn for_run(
539 page_config: &PageConfig,
540 power_of_two: bool,
541 square: bool,
542 trim_mode: impl Into<String>,
543 ) -> Self {
544 Self {
545 power_of_two,
546 square,
547 max_dimensions: page_config.max_dimensions(),
548 padding: (page_config.border_padding(), page_config.texture_padding()),
549 extrude: page_config.texture_extrusion(),
550 allow_rotation: page_config.allow_rotation(),
551 trim_mode: trim_mode.into(),
552 ..Self::default()
553 }
554 }
555
556 fn validate(&self) -> Result<()> {
557 for (name, value) in [
558 ("app", self.app.as_str()),
559 ("version", self.version.as_str()),
560 ("format", self.format.as_str()),
561 ("trim_mode", self.trim_mode.as_str()),
562 ] {
563 if value.trim().is_empty() {
564 return Err(invariant(
565 "atlas metadata",
566 format!("{name} must not be empty"),
567 ));
568 }
569 }
570 if !self.scale.is_finite() || self.scale <= 0.0 {
571 return Err(invariant(
572 "atlas metadata",
573 format!("scale must be finite and positive, got {}", self.scale),
574 ));
575 }
576 if self.max_dimensions.0 == 0 || self.max_dimensions.1 == 0 {
577 return Err(invariant(
578 "atlas metadata",
579 format!(
580 "maximum dimensions must be positive, got {}x{}",
581 self.max_dimensions.0, self.max_dimensions.1
582 ),
583 ));
584 }
585
586 let border = self.padding.0;
587 let texture_padding = self.padding.1;
588 let doubled_border = border.checked_mul(2).ok_or_else(|| {
589 invariant(
590 "atlas metadata",
591 format!("border padding {border} overflows page geometry"),
592 )
593 })?;
594 let usable_width = self.max_dimensions.0.checked_sub(doubled_border);
595 let usable_height = self.max_dimensions.1.checked_sub(doubled_border);
596 let (Some(usable_width), Some(usable_height)) = (usable_width, usable_height) else {
597 return Err(invariant(
598 "atlas metadata",
599 format!(
600 "border padding {border} exceeds maximum dimensions {}x{}",
601 self.max_dimensions.0, self.max_dimensions.1
602 ),
603 ));
604 };
605 if usable_width == 0 || usable_height == 0 {
606 return Err(invariant(
607 "atlas metadata",
608 "border padding leaves no usable page area",
609 ));
610 }
611
612 let allocation_extra = self
613 .extrude
614 .checked_mul(2)
615 .and_then(|extrusion| extrusion.checked_add(texture_padding))
616 .ok_or_else(|| {
617 invariant(
618 "atlas metadata",
619 format!(
620 "texture padding {texture_padding} and extrusion {} overflow allocation geometry",
621 self.extrude
622 ),
623 )
624 })?;
625 let minimum_reservation = allocation_extra.checked_add(1).ok_or_else(|| {
626 invariant(
627 "atlas metadata",
628 "minimum texture reservation overflows allocation geometry",
629 )
630 })?;
631 if minimum_reservation > usable_width || minimum_reservation > usable_height {
632 return Err(invariant(
633 "atlas metadata",
634 format!(
635 "padding and extrusion require at least {minimum_reservation}x{minimum_reservation} usable pixels, got {usable_width}x{usable_height}"
636 ),
637 ));
638 }
639 Ok(())
640 }
641
642 fn expand_max_dimensions_to_cover(&mut self, pages: &[Page]) {
643 for page in pages {
644 self.max_dimensions.0 = self.max_dimensions.0.max(page.width);
645 self.max_dimensions.1 = self.max_dimensions.1.max(page.height);
646 }
647 }
648
649 fn validate_against_pages(&self, pages: &[Page]) -> Result<()> {
650 for page in pages {
651 let context = format!("page {}", page.id);
652 if page.width > self.max_dimensions.0 || page.height > self.max_dimensions.1 {
653 return Err(invariant(
654 context,
655 format!(
656 "dimensions {}x{} exceed metadata maximum {}x{}",
657 page.width, page.height, self.max_dimensions.0, self.max_dimensions.1
658 ),
659 ));
660 }
661 if self.power_of_two
662 && (!page.width.is_power_of_two() || !page.height.is_power_of_two())
663 {
664 return Err(invariant(
665 context,
666 format!(
667 "dimensions {}x{} are not both powers of two as declared by atlas metadata",
668 page.width, page.height
669 ),
670 ));
671 }
672 if self.square && page.width != page.height {
673 return Err(invariant(
674 context,
675 format!(
676 "dimensions {}x{} are not square as declared by atlas metadata",
677 page.width, page.height
678 ),
679 ));
680 }
681 if !self.allow_rotation
682 && let Some(region) = page.regions.iter().find(|region| region.rotated)
683 {
684 return Err(invariant(
685 format!("region {} on page {}", region.id, page.id),
686 "rotation is disabled by atlas metadata",
687 ));
688 }
689 }
690 Ok(())
691 }
692}
693
694impl Default for Meta {
695 fn default() -> Self {
696 Self {
697 app: "tex-packer".into(),
698 version: env!("CARGO_PKG_VERSION").into(),
699 format: "RGBA8888".into(),
700 scale: 1.0,
701 power_of_two: false,
702 square: false,
703 max_dimensions: (1024, 1024),
704 padding: (0, 2),
705 extrude: 0,
706 allow_rotation: true,
707 trim_mode: "none".into(),
708 background_color: None,
709 }
710 }
711}
712
713#[derive(Debug, Clone, PartialEq, Eq)]
714struct AtlasIndex {
715 page_by_id: HashMap<PageId, usize>,
716}
717
718#[derive(Debug, Clone, PartialEq)]
720pub struct Atlas {
721 pages: Vec<Page>,
722 meta: Meta,
723 index: AtlasIndex,
724}
725
726impl Atlas {
727 pub fn try_new(pages: Vec<Page>, mut meta: Meta) -> Result<Self> {
730 meta.validate()?;
731 meta.expand_max_dimensions_to_cover(&pages);
732 meta.validate_against_pages(&pages)?;
733 let mut page_by_id = HashMap::with_capacity(pages.len());
734 for (slot, page) in pages.iter().enumerate() {
735 if page_by_id.insert(page.id, slot).is_some() {
736 return Err(invariant(
737 format!("page {}", page.id),
738 "duplicate page identity",
739 ));
740 }
741 }
742
743 Ok(Self {
744 pages,
745 meta,
746 index: AtlasIndex { page_by_id },
747 })
748 }
749
750 pub fn pages(&self) -> &[Page] {
751 &self.pages
752 }
753
754 pub fn page(&self, id: PageId) -> Option<&Page> {
755 self.index
756 .page_by_id
757 .get(&id)
758 .and_then(|&slot| self.pages.get(slot))
759 }
760
761 pub const fn meta(&self) -> &Meta {
762 &self.meta
763 }
764
765 pub fn stats(&self) -> PackStats {
766 let mut num_frames = 0usize;
767 let mut num_regions = 0usize;
768 let mut num_aliases = 0usize;
769 let mut num_rotated_regions = 0usize;
770 let mut num_trimmed_frames = 0usize;
771 let mut page_area = 0u128;
772 let mut content_area = 0u128;
773 let mut allocation_area = 0u128;
774
775 for page in &self.pages {
776 num_frames += page.frames.len();
777 num_regions += page.regions.len();
778 num_aliases += page.frames.len().saturating_sub(page.regions.len());
779 page_area += u128::from(page.width) * u128::from(page.height);
780
781 for region in &page.regions {
782 content_area += region.content.area();
783 allocation_area += region.allocation.area();
784 num_rotated_regions += usize::from(region.rotated);
785 }
786 for frame in &page.frames {
787 num_trimmed_frames += usize::from(frame.trimmed);
788 }
789 }
790
791 PackStats {
792 num_pages: self.pages.len(),
793 num_frames,
794 num_regions,
795 num_aliases,
796 num_rotated_regions,
797 num_trimmed_frames,
798 page_area,
799 content_area,
800 allocation_area,
801 content_occupancy: area_ratio(content_area, page_area),
802 allocation_occupancy: area_ratio(allocation_area, page_area),
803 }
804 }
805}
806
807#[derive(Debug, Clone, Copy, PartialEq)]
809pub struct PackStats {
810 pub num_pages: usize,
811 pub num_frames: usize,
812 pub num_regions: usize,
813 pub num_aliases: usize,
814 pub num_rotated_regions: usize,
815 pub num_trimmed_frames: usize,
816 pub page_area: u128,
817 pub content_area: u128,
818 pub allocation_area: u128,
819 pub content_occupancy: f64,
820 pub allocation_occupancy: f64,
821}
822
823impl PackStats {
824 pub fn summary(self) -> String {
825 format!(
826 "Pages: {}, Frames: {}, Regions: {}, Aliases: {}, Content occupancy: {:.2}%, Allocation occupancy: {:.2}%, Page area: {}, Content area: {}, Allocation area: {}, Rotated regions: {}, Trimmed frames: {}",
827 self.num_pages,
828 self.num_frames,
829 self.num_regions,
830 self.num_aliases,
831 self.content_occupancy * 100.0,
832 self.allocation_occupancy * 100.0,
833 self.page_area,
834 self.content_area,
835 self.allocation_area,
836 self.num_rotated_regions,
837 self.num_trimmed_frames,
838 )
839 }
840
841 pub const fn unallocated_area(self) -> u128 {
842 self.page_area.saturating_sub(self.allocation_area)
843 }
844
845 pub fn allocation_waste_percentage(self) -> f64 {
846 area_percentage(self.unallocated_area(), self.page_area)
847 }
848}
849
850pub(crate) fn area_ratio(area: u128, page_area: u128) -> f64 {
851 if page_area == 0 {
852 0.0
853 } else {
854 area as f64 / page_area as f64
855 }
856}
857
858pub(crate) fn area_percentage(area: u128, page_area: u128) -> f64 {
859 area_ratio(area, page_area) * 100.0
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
864#[serde(deny_unknown_fields)]
865pub struct AtlasDocument {
866 schema_version: u32,
867 meta: MetaDocument,
868 pages: Vec<PageDocument>,
869}
870
871impl AtlasDocument {
872 pub const SCHEMA_VERSION: u32 = 2;
873
874 pub fn from_atlas(atlas: &Atlas) -> Self {
875 Self {
876 schema_version: Self::SCHEMA_VERSION,
877 meta: MetaDocument::from(&atlas.meta),
878 pages: atlas.pages.iter().map(PageDocument::from).collect(),
879 }
880 }
881
882 pub const fn schema_version(&self) -> u32 {
883 self.schema_version
884 }
885
886 pub fn try_into_atlas(self) -> Result<Atlas> {
887 if self.schema_version != Self::SCHEMA_VERSION {
888 return Err(TexPackerError::InvalidDocument {
889 reason: format!(
890 "unsupported schema version {}; expected {}",
891 self.schema_version,
892 Self::SCHEMA_VERSION
893 ),
894 });
895 }
896
897 let pages = self
898 .pages
899 .into_iter()
900 .map(PageDocument::try_into_page)
901 .collect::<Result<Vec<_>>>()?;
902 let meta = Meta::from(self.meta);
903 meta.validate().map_err(as_document_error)?;
904 meta.validate_against_pages(&pages)
905 .map_err(as_document_error)?;
906 Atlas::try_new(pages, meta).map_err(as_document_error)
907 }
908}
909
910#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
911#[serde(deny_unknown_fields)]
912struct MetaDocument {
913 app: String,
914 version: String,
915 format: String,
916 scale: f32,
917 power_of_two: bool,
918 square: bool,
919 max_dimensions: (u32, u32),
920 padding: (u32, u32),
921 extrude: u32,
922 allow_rotation: bool,
923 trim_mode: String,
924 background_color: Option<[u8; 4]>,
925}
926
927impl From<&Meta> for MetaDocument {
928 fn from(meta: &Meta) -> Self {
929 Self {
930 app: meta.app.clone(),
931 version: meta.version.clone(),
932 format: meta.format.clone(),
933 scale: meta.scale,
934 power_of_two: meta.power_of_two,
935 square: meta.square,
936 max_dimensions: meta.max_dimensions,
937 padding: meta.padding,
938 extrude: meta.extrude,
939 allow_rotation: meta.allow_rotation,
940 trim_mode: meta.trim_mode.clone(),
941 background_color: meta.background_color,
942 }
943 }
944}
945
946impl From<MetaDocument> for Meta {
947 fn from(document: MetaDocument) -> Self {
948 Self {
949 app: document.app,
950 version: document.version,
951 format: document.format,
952 scale: document.scale,
953 power_of_two: document.power_of_two,
954 square: document.square,
955 max_dimensions: document.max_dimensions,
956 padding: document.padding,
957 extrude: document.extrude,
958 allow_rotation: document.allow_rotation,
959 trim_mode: document.trim_mode,
960 background_color: document.background_color,
961 }
962 }
963}
964
965#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
966#[serde(deny_unknown_fields)]
967struct PageDocument {
968 id: u32,
969 width: u32,
970 height: u32,
971 regions: Vec<RegionDocument>,
972 frames: Vec<FrameDocument>,
973}
974
975impl PageDocument {
976 fn try_into_page(self) -> Result<Page> {
977 Page::try_new(
978 PageId::new(self.id),
979 self.width,
980 self.height,
981 self.regions
982 .into_iter()
983 .map(RegionDocument::into_region)
984 .collect(),
985 self.frames
986 .into_iter()
987 .map(FrameDocument::into_frame)
988 .collect(),
989 )
990 .map_err(as_document_error)
991 }
992}
993
994impl From<&Page> for PageDocument {
995 fn from(page: &Page) -> Self {
996 Self {
997 id: page.id.get(),
998 width: page.width,
999 height: page.height,
1000 regions: page.regions.iter().map(RegionDocument::from).collect(),
1001 frames: page.frames.iter().map(FrameDocument::from).collect(),
1002 }
1003 }
1004}
1005
1006#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1007#[serde(deny_unknown_fields)]
1008struct RegionDocument {
1009 id: u32,
1010 content: RectDocument,
1011 allocation: RectDocument,
1012 rotated: bool,
1013}
1014
1015impl RegionDocument {
1016 fn into_region(self) -> Region {
1017 Region::new(
1018 RegionId::new(self.id),
1019 self.content.into(),
1020 self.allocation.into(),
1021 self.rotated,
1022 )
1023 }
1024}
1025
1026impl From<&Region> for RegionDocument {
1027 fn from(region: &Region) -> Self {
1028 Self {
1029 id: region.id.get(),
1030 content: region.content.into(),
1031 allocation: region.allocation.into(),
1032 rotated: region.rotated,
1033 }
1034 }
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1038#[serde(deny_unknown_fields)]
1039struct FrameDocument {
1040 id: u32,
1041 key: String,
1042 region_id: u32,
1043 trimmed: bool,
1044 source: RectDocument,
1045 source_size: (u32, u32),
1046}
1047
1048impl FrameDocument {
1049 fn into_frame(self) -> Frame {
1050 Frame::new(
1051 FrameId::new(self.id),
1052 self.key,
1053 RegionId::new(self.region_id),
1054 self.trimmed,
1055 self.source.into(),
1056 self.source_size,
1057 )
1058 }
1059}
1060
1061impl From<&Frame> for FrameDocument {
1062 fn from(frame: &Frame) -> Self {
1063 Self {
1064 id: frame.id.get(),
1065 key: frame.key.clone(),
1066 region_id: frame.region_id.get(),
1067 trimmed: frame.trimmed,
1068 source: frame.source.into(),
1069 source_size: frame.source_size,
1070 }
1071 }
1072}
1073
1074#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1075#[serde(deny_unknown_fields)]
1076struct RectDocument {
1077 x: u32,
1078 y: u32,
1079 w: u32,
1080 h: u32,
1081}
1082
1083impl From<Rect> for RectDocument {
1084 fn from(rect: Rect) -> Self {
1085 Self {
1086 x: rect.x,
1087 y: rect.y,
1088 w: rect.w,
1089 h: rect.h,
1090 }
1091 }
1092}
1093
1094impl From<RectDocument> for Rect {
1095 fn from(document: RectDocument) -> Self {
1096 Self::new(document.x, document.y, document.w, document.h)
1097 }
1098}
1099
1100fn invariant(context: impl Into<String>, reason: impl Into<String>) -> TexPackerError {
1101 TexPackerError::InvariantViolation {
1102 context: context.into(),
1103 reason: reason.into(),
1104 }
1105}
1106
1107fn region_context(page_id: PageId, region_id: RegionId) -> String {
1108 format!("page {page_id}, region {region_id}")
1109}
1110
1111fn frame_context(page_id: PageId, frame: &Frame) -> String {
1112 format!("page {page_id}, frame {} (key {:?})", frame.id, frame.key)
1113}
1114
1115fn as_document_error(error: TexPackerError) -> TexPackerError {
1116 TexPackerError::InvalidDocument {
1117 reason: error.to_string(),
1118 }
1119}