Skip to main content

tex_packer_core/
model.rs

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/// Axis-aligned pixel rectangle. `(x, y)` is the top-left corner and `(w, h)` is its size.
10#[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    /// Inclusive right edge coordinate.
24    pub const fn right(self) -> u32 {
25        self.x.saturating_add(self.w.saturating_sub(1))
26    }
27
28    /// Inclusive bottom edge coordinate.
29    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    /// Returns whether `inner` is a positive-area rectangle fully inside this rectangle.
42    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    /// Returns whether two positive-area rectangles overlap. Touching edges do not overlap.
52    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    /// Stable atlas page identity. It is not a collection index.
97    PageId
98);
99identity_type!(
100    /// Stable physical-region identity scoped to one page.
101    RegionId
102);
103identity_type!(
104    /// Stable logical-frame identity scoped to one page.
105    FrameId
106);
107
108/// One physical allocation on an atlas page.
109#[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/// One logical sprite that resolves to a physical region on the same page.
145#[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/// Validated page aggregate containing physical regions and logical frames.
207#[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(&region.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(&region.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(&regions) {
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(&region_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 = &regions[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        // Until the first invalid start event, active y intervals are disjoint,
420        // so only the immediate neighbors can overlap the new interval.
421        if let Some((_, &other_slot)) = active_by_y.range(..=allocation.y).next_back()
422            && allocation.intersects(&regions[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(&regions[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/// Borrowed logical frame together with its authoritative physical region.
442#[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/// Descriptive metadata for an atlas run. Native schema versioning belongs to [`AtlasDocument`].
469#[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    /// Returns whether power-of-two output sizing was applied to every page.
503    pub const fn power_of_two(&self) -> bool {
504        self.power_of_two
505    }
506
507    /// Returns whether square output sizing was applied to every page.
508    pub const fn square(&self) -> bool {
509        self.square
510    }
511
512    /// Returns an upper bound for every output page dimension.
513    pub const fn max_dimensions(&self) -> (u32, u32) {
514        self.max_dimensions
515    }
516
517    /// Returns `(border_padding, texture_padding)`.
518    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/// Validated texture-atlas aggregate.
719#[derive(Debug, Clone, PartialEq)]
720pub struct Atlas {
721    pages: Vec<Page>,
722    meta: Meta,
723    index: AtlasIndex,
724}
725
726impl Atlas {
727    /// Builds a validated aggregate, expanding descriptive maximum dimensions to cover its pages.
728    /// Other metadata guarantees must already match the supplied geometry.
729    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/// Statistics derived from unique physical regions and logical frames.
808#[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/// Reversible, versioned persistence representation of an [`Atlas`].
863#[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}