Skip to main content

tex_packer_core/
config.rs

1use std::str::FromStr;
2use std::time::Duration;
3
4use crate::error::{Result, TexPackerError};
5
6/// MaxRects placement heuristics.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
8pub enum MaxRectsHeuristic {
9    #[default]
10    BestAreaFit,
11    BestShortSideFit,
12    BestLongSideFit,
13    BottomLeft,
14    ContactPoint,
15}
16
17impl FromStr for MaxRectsHeuristic {
18    type Err = ();
19
20    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
21        match value.to_ascii_lowercase().as_str() {
22            "baf" | "bestareafit" => Ok(Self::BestAreaFit),
23            "bssf" | "bestshortsidefit" => Ok(Self::BestShortSideFit),
24            "blsf" | "bestlongsidefit" => Ok(Self::BestLongSideFit),
25            "bl" | "bottomleft" => Ok(Self::BottomLeft),
26            "cp" | "contactpoint" => Ok(Self::ContactPoint),
27            _ => Err(()),
28        }
29    }
30}
31
32/// Skyline placement heuristics.
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
34pub enum SkylineHeuristic {
35    #[default]
36    BottomLeft,
37    MinWaste,
38}
39
40impl FromStr for SkylineHeuristic {
41    type Err = ();
42
43    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
44        match value.to_ascii_lowercase().as_str() {
45            "bl" | "bottomleft" => Ok(Self::BottomLeft),
46            "minwaste" | "mw" => Ok(Self::MinWaste),
47            _ => Err(()),
48        }
49    }
50}
51
52/// Guillotine free-rectangle choice heuristics.
53#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
54pub enum GuillotineChoice {
55    #[default]
56    BestAreaFit,
57    BestShortSideFit,
58    BestLongSideFit,
59    WorstAreaFit,
60    WorstShortSideFit,
61    WorstLongSideFit,
62}
63
64impl FromStr for GuillotineChoice {
65    type Err = ();
66
67    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
68        match value.to_ascii_lowercase().as_str() {
69            "baf" | "bestareafit" => Ok(Self::BestAreaFit),
70            "bssf" | "bestshortsidefit" => Ok(Self::BestShortSideFit),
71            "blsf" | "bestlongsidefit" => Ok(Self::BestLongSideFit),
72            "waf" | "worstareafit" => Ok(Self::WorstAreaFit),
73            "wssf" | "worstshortsidefit" => Ok(Self::WorstShortSideFit),
74            "wlsf" | "worstlongsidefit" => Ok(Self::WorstLongSideFit),
75            _ => Err(()),
76        }
77    }
78}
79
80/// Guillotine split-axis heuristics.
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
82pub enum GuillotineSplit {
83    #[default]
84    SplitShorterLeftoverAxis,
85    SplitLongerLeftoverAxis,
86    SplitMinimizeArea,
87    SplitMaximizeArea,
88    SplitShorterAxis,
89    SplitLongerAxis,
90}
91
92impl FromStr for GuillotineSplit {
93    type Err = ();
94
95    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
96        match value.to_ascii_lowercase().as_str() {
97            "slas" | "splitshorterleftoveraxis" => Ok(Self::SplitShorterLeftoverAxis),
98            "llas" | "splitlongerleftoveraxis" => Ok(Self::SplitLongerLeftoverAxis),
99            "minas" | "splitminimizearea" => Ok(Self::SplitMinimizeArea),
100            "maxas" | "splitmaximizearea" => Ok(Self::SplitMaximizeArea),
101            "sas" | "splitshorteraxis" => Ok(Self::SplitShorterAxis),
102            "las" | "splitlongeraxis" => Ok(Self::SplitLongerAxis),
103            _ => Err(()),
104        }
105    }
106}
107
108/// Auto portfolio presets.
109#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
110pub enum AutoMode {
111    Fast,
112    #[default]
113    Quality,
114}
115
116impl FromStr for AutoMode {
117    type Err = ();
118
119    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
120        match value.to_ascii_lowercase().as_str() {
121            "fast" => Ok(Self::Fast),
122            "quality" => Ok(Self::Quality),
123            _ => Err(()),
124        }
125    }
126}
127
128/// Sorting orders for deterministic offline packing.
129#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
130pub enum SortOrder {
131    #[default]
132    AreaDesc,
133    MaxSideDesc,
134    HeightDesc,
135    WidthDesc,
136    NameAsc,
137    None,
138}
139
140impl FromStr for SortOrder {
141    type Err = ();
142
143    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
144        match value.to_ascii_lowercase().as_str() {
145            "area_desc" => Ok(Self::AreaDesc),
146            "max_side_desc" => Ok(Self::MaxSideDesc),
147            "height_desc" => Ok(Self::HeightDesc),
148            "width_desc" => Ok(Self::WidthDesc),
149            "name_asc" => Ok(Self::NameAsc),
150            "none" => Ok(Self::None),
151            _ => Err(()),
152        }
153    }
154}
155
156/// Policy for fully transparent images when trimming is enabled.
157#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
158pub enum TransparentPolicy {
159    #[default]
160    Keep,
161    OneByOne,
162    Skip,
163}
164
165impl FromStr for TransparentPolicy {
166    type Err = ();
167
168    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
169        match value.to_ascii_lowercase().as_str() {
170            "keep" => Ok(Self::Keep),
171            "one_by_one" | "1x1" | "onebyone" => Ok(Self::OneByOne),
172            "skip" => Ok(Self::Skip),
173            _ => Err(()),
174        }
175    }
176}
177
178/// Shelf selection policies for runtime packing.
179#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
180pub enum ShelfPolicy {
181    #[default]
182    NextFit,
183    FirstFit,
184}
185
186impl FromStr for ShelfPolicy {
187    type Err = ();
188
189    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
190        match value.to_ascii_lowercase().as_str() {
191            "next_fit" | "nextfit" => Ok(Self::NextFit),
192            "first_fit" | "firstfit" => Ok(Self::FirstFit),
193            _ => Err(()),
194        }
195    }
196}
197
198/// A selected offline packing strategy and only the options relevant to it.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum PackingStrategy {
201    Skyline {
202        heuristic: SkylineHeuristic,
203        use_waste_map: bool,
204    },
205    MaxRects {
206        heuristic: MaxRectsHeuristic,
207        reference: bool,
208    },
209    Guillotine {
210        choice: GuillotineChoice,
211        split: GuillotineSplit,
212    },
213    Auto {
214        mode: AutoMode,
215        time_budget: Option<Duration>,
216        parallel: bool,
217        reference_time_threshold: Option<Duration>,
218        reference_input_threshold: Option<usize>,
219    },
220}
221
222impl Default for PackingStrategy {
223    fn default() -> Self {
224        Self::Skyline {
225            heuristic: SkylineHeuristic::BottomLeft,
226            use_waste_map: false,
227        }
228    }
229}
230
231/// A selected runtime packing strategy and only the options relevant to it.
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum RuntimeStrategy {
234    Guillotine {
235        choice: GuillotineChoice,
236        split: GuillotineSplit,
237    },
238    Shelf {
239        policy: ShelfPolicy,
240    },
241    Skyline {
242        heuristic: SkylineHeuristic,
243    },
244}
245
246impl Default for RuntimeStrategy {
247    fn default() -> Self {
248        Self::Guillotine {
249            choice: GuillotineChoice::BestAreaFit,
250            split: GuillotineSplit::SplitShorterLeftoverAxis,
251        }
252    }
253}
254
255/// Validated geometry shared by offline and runtime workflows.
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct PageConfig {
258    max_width: u32,
259    max_height: u32,
260    allow_rotation: bool,
261    border_padding: u32,
262    texture_padding: u32,
263    texture_extrusion: u32,
264    usable_width: u32,
265    usable_height: u32,
266    allocation_extra: u32,
267    content_offset: u32,
268    trailing_extra: u32,
269}
270
271impl PageConfig {
272    pub fn builder() -> PageConfigBuilder {
273        PageConfigBuilder::default()
274    }
275
276    pub fn max_width(&self) -> u32 {
277        self.max_width
278    }
279
280    pub fn max_height(&self) -> u32 {
281        self.max_height
282    }
283
284    pub fn max_dimensions(&self) -> (u32, u32) {
285        (self.max_width, self.max_height)
286    }
287
288    pub fn allow_rotation(&self) -> bool {
289        self.allow_rotation
290    }
291
292    pub fn border_padding(&self) -> u32 {
293        self.border_padding
294    }
295
296    pub fn texture_padding(&self) -> u32 {
297        self.texture_padding
298    }
299
300    pub fn texture_extrusion(&self) -> u32 {
301        self.texture_extrusion
302    }
303
304    pub(crate) fn usable_dimensions(&self) -> (u32, u32) {
305        (self.usable_width, self.usable_height)
306    }
307
308    pub(crate) fn content_offset(&self) -> u32 {
309        self.content_offset
310    }
311
312    pub(crate) fn trailing_extra(&self) -> u32 {
313        self.trailing_extra
314    }
315
316    pub(crate) fn checked_reservation(
317        &self,
318        content_width: u32,
319        content_height: u32,
320    ) -> Option<(u32, u32)> {
321        Some((
322            content_width.checked_add(self.allocation_extra)?,
323            content_height.checked_add(self.allocation_extra)?,
324        ))
325    }
326}
327
328impl Default for PageConfig {
329    fn default() -> Self {
330        Self {
331            max_width: 1024,
332            max_height: 1024,
333            allow_rotation: true,
334            border_padding: 0,
335            texture_padding: 2,
336            texture_extrusion: 0,
337            usable_width: 1024,
338            usable_height: 1024,
339            allocation_extra: 2,
340            content_offset: 1,
341            trailing_extra: 1,
342        }
343    }
344}
345
346/// Builder for [`PageConfig`].
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct PageConfigBuilder {
349    max_width: u32,
350    max_height: u32,
351    allow_rotation: bool,
352    border_padding: u32,
353    texture_padding: u32,
354    texture_extrusion: u32,
355}
356
357impl Default for PageConfigBuilder {
358    fn default() -> Self {
359        Self {
360            max_width: 1024,
361            max_height: 1024,
362            allow_rotation: true,
363            border_padding: 0,
364            texture_padding: 2,
365            texture_extrusion: 0,
366        }
367    }
368}
369
370impl PageConfigBuilder {
371    pub fn new() -> Self {
372        Self::default()
373    }
374
375    pub fn max_dimensions(mut self, width: u32, height: u32) -> Self {
376        self.max_width = width;
377        self.max_height = height;
378        self
379    }
380
381    pub fn allow_rotation(mut self, enabled: bool) -> Self {
382        self.allow_rotation = enabled;
383        self
384    }
385
386    pub fn border_padding(mut self, pixels: u32) -> Self {
387        self.border_padding = pixels;
388        self
389    }
390
391    pub fn texture_padding(mut self, pixels: u32) -> Self {
392        self.texture_padding = pixels;
393        self
394    }
395
396    pub fn texture_extrusion(mut self, pixels: u32) -> Self {
397        self.texture_extrusion = pixels;
398        self
399    }
400
401    pub fn build(self) -> Result<PageConfig> {
402        if self.max_width == 0 || self.max_height == 0 {
403            return Err(TexPackerError::InvalidDimensions {
404                width: self.max_width,
405                height: self.max_height,
406            });
407        }
408
409        let border_twice = self.border_padding.checked_mul(2).ok_or_else(|| {
410            invalid_config("border_padding overflows while reserving both page edges")
411        })?;
412        if border_twice >= self.max_width || border_twice >= self.max_height {
413            return Err(invalid_config(format!(
414                "border_padding ({}) leaves no usable area in {}x{}",
415                self.border_padding, self.max_width, self.max_height
416            )));
417        }
418
419        let usable_width = self.max_width - border_twice;
420        let usable_height = self.max_height - border_twice;
421        let allocation_extra = self
422            .texture_extrusion
423            .checked_mul(2)
424            .and_then(|value| value.checked_add(self.texture_padding))
425            .ok_or_else(|| {
426                invalid_config(
427                    "texture padding and extrusion overflow the coordinate representation",
428                )
429            })?;
430        let leading_padding = self.texture_padding / 2;
431        let trailing_padding = self.texture_padding - leading_padding;
432        let content_offset = self
433            .texture_extrusion
434            .checked_add(leading_padding)
435            .ok_or_else(|| {
436                invalid_config("content offset overflows the coordinate representation")
437            })?;
438        let trailing_extra = self
439            .texture_extrusion
440            .checked_add(trailing_padding)
441            .ok_or_else(|| {
442                invalid_config("trailing reservation overflows the coordinate representation")
443            })?;
444        let minimum_reservation = allocation_extra.checked_add(1).ok_or_else(|| {
445            invalid_config(
446                "a one-pixel texture reservation overflows the coordinate representation",
447            )
448        })?;
449
450        if minimum_reservation > usable_width || minimum_reservation > usable_height {
451            return Err(invalid_config(format!(
452                "padding and extrusion require at least {minimum_reservation}x{minimum_reservation} pixels, but only {usable_width}x{usable_height} are usable"
453            )));
454        }
455
456        Ok(PageConfig {
457            max_width: self.max_width,
458            max_height: self.max_height,
459            allow_rotation: self.allow_rotation,
460            border_padding: self.border_padding,
461            texture_padding: self.texture_padding,
462            texture_extrusion: self.texture_extrusion,
463            usable_width,
464            usable_height,
465            allocation_extra,
466            content_offset,
467            trailing_extra,
468        })
469    }
470}
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq)]
473enum TrimProjection {
474    Disabled,
475    Enabled {
476        threshold: u8,
477        transparent_policy: TransparentPolicy,
478    },
479}
480
481/// Validated configuration for offline image and layout workflows.
482#[derive(Debug, Clone, PartialEq, Eq)]
483pub struct OfflineConfig {
484    page: PageConfig,
485    force_max_dimensions: bool,
486    power_of_two: bool,
487    square: bool,
488    trim: TrimProjection,
489    outlines: bool,
490    sort_order: SortOrder,
491    strategy: PackingStrategy,
492}
493
494impl OfflineConfig {
495    pub fn builder() -> OfflineConfigBuilder {
496        OfflineConfigBuilder::default()
497    }
498
499    pub fn page_config(&self) -> &PageConfig {
500        &self.page
501    }
502
503    pub fn force_max_dimensions(&self) -> bool {
504        self.force_max_dimensions
505    }
506
507    pub fn power_of_two(&self) -> bool {
508        self.power_of_two
509    }
510
511    pub fn square(&self) -> bool {
512        self.square
513    }
514
515    pub fn trim_enabled(&self) -> bool {
516        matches!(self.trim, TrimProjection::Enabled { .. })
517    }
518
519    pub fn trim_threshold(&self) -> Option<u8> {
520        match self.trim {
521            TrimProjection::Disabled => None,
522            TrimProjection::Enabled { threshold, .. } => Some(threshold),
523        }
524    }
525
526    pub fn transparent_policy(&self) -> Option<TransparentPolicy> {
527        match self.trim {
528            TrimProjection::Disabled => None,
529            TrimProjection::Enabled {
530                transparent_policy, ..
531            } => Some(transparent_policy),
532        }
533    }
534
535    pub fn outlines(&self) -> bool {
536        self.outlines
537    }
538
539    pub fn sort_order(&self) -> SortOrder {
540        self.sort_order
541    }
542
543    pub fn strategy(&self) -> &PackingStrategy {
544        &self.strategy
545    }
546}
547
548impl Default for OfflineConfig {
549    fn default() -> Self {
550        Self {
551            page: PageConfig::default(),
552            force_max_dimensions: false,
553            power_of_two: false,
554            square: false,
555            trim: TrimProjection::Enabled {
556                threshold: 0,
557                transparent_policy: TransparentPolicy::Keep,
558            },
559            outlines: false,
560            sort_order: SortOrder::AreaDesc,
561            strategy: PackingStrategy::default(),
562        }
563    }
564}
565
566/// Builder for [`OfflineConfig`].
567#[derive(Debug, Clone, PartialEq, Eq)]
568pub struct OfflineConfigBuilder {
569    page: PageConfig,
570    force_max_dimensions: bool,
571    power_of_two: bool,
572    square: bool,
573    trim: bool,
574    trim_threshold: u8,
575    transparent_policy: TransparentPolicy,
576    outlines: bool,
577    sort_order: SortOrder,
578    strategy: PackingStrategy,
579}
580
581impl Default for OfflineConfigBuilder {
582    fn default() -> Self {
583        Self {
584            page: PageConfig::default(),
585            force_max_dimensions: false,
586            power_of_two: false,
587            square: false,
588            trim: true,
589            trim_threshold: 0,
590            transparent_policy: TransparentPolicy::Keep,
591            outlines: false,
592            sort_order: SortOrder::AreaDesc,
593            strategy: PackingStrategy::default(),
594        }
595    }
596}
597
598impl OfflineConfigBuilder {
599    pub fn new() -> Self {
600        Self::default()
601    }
602
603    pub fn page_config(mut self, config: PageConfig) -> Self {
604        self.page = config;
605        self
606    }
607
608    pub fn force_max_dimensions(mut self, enabled: bool) -> Self {
609        self.force_max_dimensions = enabled;
610        self
611    }
612
613    pub fn power_of_two(mut self, enabled: bool) -> Self {
614        self.power_of_two = enabled;
615        self
616    }
617
618    pub fn square(mut self, enabled: bool) -> Self {
619        self.square = enabled;
620        self
621    }
622
623    pub fn trim(mut self, enabled: bool) -> Self {
624        self.trim = enabled;
625        self
626    }
627
628    pub fn trim_threshold(mut self, threshold: u8) -> Self {
629        self.trim_threshold = threshold;
630        self
631    }
632
633    pub fn transparent_policy(mut self, policy: TransparentPolicy) -> Self {
634        self.transparent_policy = policy;
635        self
636    }
637
638    pub fn outlines(mut self, enabled: bool) -> Self {
639        self.outlines = enabled;
640        self
641    }
642
643    pub fn sort_order(mut self, order: SortOrder) -> Self {
644        self.sort_order = order;
645        self
646    }
647
648    pub fn strategy(mut self, strategy: PackingStrategy) -> Self {
649        self.strategy = strategy;
650        self
651    }
652
653    pub fn build(self) -> Result<OfflineConfig> {
654        if self.power_of_two && !self.force_max_dimensions {
655            self.page
656                .max_width
657                .checked_next_power_of_two()
658                .ok_or_else(|| {
659                    invalid_config(format!(
660                        "power-of-two page width overflows for maximum width {}",
661                        self.page.max_width
662                    ))
663                })?;
664            self.page
665                .max_height
666                .checked_next_power_of_two()
667                .ok_or_else(|| {
668                    invalid_config(format!(
669                        "power-of-two page height overflows for maximum height {}",
670                        self.page.max_height
671                    ))
672                })?;
673        }
674
675        let trim = if self.trim {
676            TrimProjection::Enabled {
677                threshold: self.trim_threshold,
678                transparent_policy: self.transparent_policy,
679            }
680        } else {
681            TrimProjection::Disabled
682        };
683        let strategy = normalize_strategy(self.strategy);
684
685        Ok(OfflineConfig {
686            page: self.page,
687            force_max_dimensions: self.force_max_dimensions,
688            power_of_two: self.power_of_two,
689            square: self.square,
690            trim,
691            outlines: self.outlines,
692            sort_order: self.sort_order,
693            strategy,
694        })
695    }
696}
697
698fn normalize_strategy(strategy: PackingStrategy) -> PackingStrategy {
699    match strategy {
700        PackingStrategy::Auto {
701            mode,
702            time_budget,
703            parallel,
704            reference_time_threshold,
705            reference_input_threshold,
706        } => PackingStrategy::Auto {
707            mode,
708            time_budget: time_budget.filter(|budget| !budget.is_zero()),
709            parallel,
710            reference_time_threshold,
711            reference_input_threshold,
712        },
713        other => other,
714    }
715}
716
717/// Validated configuration for runtime atlas workflows.
718#[derive(Debug, Clone, Default, PartialEq, Eq)]
719pub struct RuntimeConfig {
720    page: PageConfig,
721    strategy: RuntimeStrategy,
722}
723
724impl RuntimeConfig {
725    pub fn builder() -> RuntimeConfigBuilder {
726        RuntimeConfigBuilder::default()
727    }
728
729    pub fn page_config(&self) -> &PageConfig {
730        &self.page
731    }
732
733    pub fn strategy(&self) -> &RuntimeStrategy {
734        &self.strategy
735    }
736}
737
738/// Builder for [`RuntimeConfig`].
739#[derive(Debug, Clone, Default, PartialEq, Eq)]
740pub struct RuntimeConfigBuilder {
741    page: PageConfig,
742    strategy: RuntimeStrategy,
743}
744
745impl RuntimeConfigBuilder {
746    pub fn new() -> Self {
747        Self::default()
748    }
749
750    pub fn page_config(mut self, config: PageConfig) -> Self {
751        self.page = config;
752        self
753    }
754
755    pub fn strategy(mut self, strategy: RuntimeStrategy) -> Self {
756        self.strategy = strategy;
757        self
758    }
759
760    pub fn build(self) -> Result<RuntimeConfig> {
761        Ok(RuntimeConfig {
762            page: self.page,
763            strategy: self.strategy,
764        })
765    }
766}
767
768fn invalid_config(message: impl Into<String>) -> TexPackerError {
769    TexPackerError::InvalidConfig(message.into())
770}