tex_packer_core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::str::FromStr;
3
4/// Algorithm families and packing configuration.
5/// Key notes:
6///   - `family` selects Skyline/MaxRects/Guillotine/Auto
7///   - `mr_reference` toggles reference-accurate MaxRects split/prune (SplitFreeNode), improving packing on large sets at higher CPU cost
8///   - `time_budget_ms` and `parallel` affect Auto portfolio evaluation
9///     Top-level algorithm families.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "lowercase")]
12pub enum AlgorithmFamily {
13    /// Skyline data structure (BL/MW; fast and good baseline). Optional waste-map recovery.
14    Skyline,
15    /// MaxRects free-list (high quality; many heuristics; best for offline).
16    MaxRects,
17    /// Guillotine splitting (flexible choice/split; competitive; useful in waste-map too).
18    Guillotine,
19    /// Try a small portfolio of candidates and pick the best result (pages, then total area).
20    Auto,
21}
22
23impl FromStr for AlgorithmFamily {
24    type Err = ();
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        match s.to_ascii_lowercase().as_str() {
27            "skyline" => Ok(Self::Skyline),
28            "maxrects" => Ok(Self::MaxRects),
29            "guillotine" => Ok(Self::Guillotine),
30            "auto" => Ok(Self::Auto),
31            _ => Err(()),
32        }
33    }
34}
35
36/// MaxRects placement heuristics.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
38#[serde(rename_all = "lowercase")]
39pub enum MaxRectsHeuristic {
40    BestAreaFit,
41    BestShortSideFit,
42    BestLongSideFit,
43    BottomLeft,
44    ContactPoint,
45}
46
47impl FromStr for MaxRectsHeuristic {
48    type Err = ();
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        match s.to_ascii_lowercase().as_str() {
51            "baf" | "bestareafit" => Ok(Self::BestAreaFit),
52            "bssf" | "bestshortsidefit" => Ok(Self::BestShortSideFit),
53            "blsf" | "bestlongsidefit" => Ok(Self::BestLongSideFit),
54            "bl" | "bottomleft" => Ok(Self::BottomLeft),
55            "cp" | "contactpoint" => Ok(Self::ContactPoint),
56            _ => Err(()),
57        }
58    }
59}
60
61/// Skyline placement heuristics.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "lowercase")]
64pub enum SkylineHeuristic {
65    BottomLeft,
66    MinWaste,
67}
68
69impl FromStr for SkylineHeuristic {
70    type Err = ();
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        match s.to_ascii_lowercase().as_str() {
73            "bl" | "bottomleft" => Ok(Self::BottomLeft),
74            "minwaste" | "mw" => Ok(Self::MinWaste),
75            _ => Err(()),
76        }
77    }
78}
79
80/// Guillotine free-rect choice heuristics.
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "lowercase")]
83pub enum GuillotineChoice {
84    BestAreaFit,
85    BestShortSideFit,
86    BestLongSideFit,
87    WorstAreaFit,
88    WorstShortSideFit,
89    WorstLongSideFit,
90}
91
92impl FromStr for GuillotineChoice {
93    type Err = ();
94    fn from_str(s: &str) -> Result<Self, Self::Err> {
95        match s.to_ascii_lowercase().as_str() {
96            "baf" | "bestareafit" => Ok(Self::BestAreaFit),
97            "bssf" | "bestshortsidefit" => Ok(Self::BestShortSideFit),
98            "blsf" | "bestlongsidefit" => Ok(Self::BestLongSideFit),
99            "waf" | "worstareafit" => Ok(Self::WorstAreaFit),
100            "wssf" | "worstshortsidefit" => Ok(Self::WorstShortSideFit),
101            "wlsf" | "worstlongsidefit" => Ok(Self::WorstLongSideFit),
102            _ => Err(()),
103        }
104    }
105}
106
107/// Guillotine split axis heuristics.
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
109#[serde(rename_all = "lowercase")]
110pub enum GuillotineSplit {
111    SplitShorterLeftoverAxis,
112    SplitLongerLeftoverAxis,
113    SplitMinimizeArea,
114    SplitMaximizeArea,
115    SplitShorterAxis,
116    SplitLongerAxis,
117}
118
119impl FromStr for GuillotineSplit {
120    type Err = ();
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        match s.to_ascii_lowercase().as_str() {
123            "slas" | "splitshorterleftoveraxis" => Ok(Self::SplitShorterLeftoverAxis),
124            "llas" | "splitlongerleftoveraxis" => Ok(Self::SplitLongerLeftoverAxis),
125            "minas" | "splitminimizearea" => Ok(Self::SplitMinimizeArea),
126            "maxas" | "splitmaximizearea" => Ok(Self::SplitMaximizeArea),
127            "sas" | "splitshorteraxis" => Ok(Self::SplitShorterAxis),
128            "las" | "splitlongeraxis" => Ok(Self::SplitLongerAxis),
129            _ => Err(()),
130        }
131    }
132}
133
134/// Auto presets.
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
136#[serde(rename_all = "lowercase")]
137pub enum AutoMode {
138    Fast,
139    Quality,
140}
141
142impl FromStr for AutoMode {
143    type Err = ();
144    fn from_str(s: &str) -> Result<Self, Self::Err> {
145        match s.to_ascii_lowercase().as_str() {
146            "fast" => Ok(Self::Fast),
147            "quality" => Ok(Self::Quality),
148            _ => Err(()),
149        }
150    }
151}
152
153/// Sorting orders for deterministic packing.
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
155#[serde(rename_all = "snake_case")]
156pub enum SortOrder {
157    AreaDesc,
158    MaxSideDesc,
159    HeightDesc,
160    WidthDesc,
161    NameAsc,
162    None,
163}
164
165impl FromStr for SortOrder {
166    type Err = ();
167    fn from_str(s: &str) -> Result<Self, Self::Err> {
168        match s.to_ascii_lowercase().as_str() {
169            "area_desc" => Ok(Self::AreaDesc),
170            "max_side_desc" => Ok(Self::MaxSideDesc),
171            "height_desc" => Ok(Self::HeightDesc),
172            "width_desc" => Ok(Self::WidthDesc),
173            "name_asc" => Ok(Self::NameAsc),
174            "none" => Ok(Self::None),
175            _ => Err(()),
176        }
177    }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct PackerConfig {
182    /// Maximum page width in pixels.
183    pub max_width: u32,
184    /// Maximum page height in pixels.
185    pub max_height: u32,
186    /// Allow 90° rotations for placements where beneficial.
187    pub allow_rotation: bool,
188    /// Force final page dimensions to be exactly max_width/max_height.
189    pub force_max_dimensions: bool,
190
191    /// Pixels around entire page border.
192    pub border_padding: u32,
193    /// Pixels between frames.
194    pub texture_padding: u32,
195    /// Extrude edge pixels of each frame (for sampling safety).
196    pub texture_extrusion: u32,
197
198    /// Trim transparent borders (alpha <= trim_threshold).
199    pub trim: bool,
200    pub trim_threshold: u8,
201    /// Draw red outlines on output pages (debug).
202    pub texture_outlines: bool,
203
204    /// Resize output page to power-of-two.
205    pub power_of_two: bool,
206    /// Force output page to be square (max(width,height)).
207    pub square: bool,
208    /// Use waste map in Skyline to recover gaps
209    pub use_waste_map: bool,
210
211    // algorithm selection
212    #[serde(default = "default_family")]
213    pub family: AlgorithmFamily,
214    #[serde(default = "default_mr_heuristic")]
215    pub mr_heuristic: MaxRectsHeuristic,
216    #[serde(default = "default_skyline_heuristic")]
217    pub skyline_heuristic: SkylineHeuristic,
218    #[serde(default = "default_g_choice")]
219    pub g_choice: GuillotineChoice,
220    #[serde(default = "default_g_split")]
221    pub g_split: GuillotineSplit,
222    #[serde(default = "default_auto_mode")]
223    pub auto_mode: AutoMode,
224    #[serde(default = "default_sort_order")]
225    pub sort_order: SortOrder,
226
227    // portfolio/parallel controls
228    /// Optional time budget for auto portfolio (milliseconds). None or 0 disables.
229    #[serde(default)]
230    pub time_budget_ms: Option<u64>,
231    /// Enable parallel candidate evaluation when feature "parallel" is on.
232    #[serde(default = "default_parallel")]
233    pub parallel: bool,
234
235    /// Use reference-accurate MaxRects split/prune (SplitFreeNode + staged prune).
236    /// When false, uses a simpler but correct split/prune that may create more intermediate free rects.
237    #[serde(default)]
238    pub mr_reference: bool,
239
240    /// Auto-mode: enable mr_reference when time budget >= this (ms). None => use default heuristic.
241    #[serde(default)]
242    pub auto_mr_ref_time_ms_threshold: Option<u64>,
243    /// Auto-mode: enable mr_reference when inputs >= this count. None => use default heuristic.
244    #[serde(default)]
245    pub auto_mr_ref_input_threshold: Option<usize>,
246
247    /// Policy for fully transparent images (effective when `trim=true`).
248    #[serde(default = "default_transparent_policy")]
249    pub transparent_policy: TransparentPolicy,
250}
251
252impl Default for PackerConfig {
253    fn default() -> Self {
254        Self {
255            max_width: 1024,
256            max_height: 1024,
257            allow_rotation: true,
258            force_max_dimensions: false,
259            border_padding: 0,
260            texture_padding: 2,
261            texture_extrusion: 0,
262            trim: true,
263            trim_threshold: 0,
264            texture_outlines: false,
265            power_of_two: false,
266            square: false,
267            use_waste_map: false,
268            family: default_family(),
269            mr_heuristic: default_mr_heuristic(),
270            skyline_heuristic: default_skyline_heuristic(),
271            g_choice: default_g_choice(),
272            g_split: default_g_split(),
273            auto_mode: default_auto_mode(),
274            sort_order: default_sort_order(),
275            time_budget_ms: None,
276            parallel: default_parallel(),
277            mr_reference: false,
278            auto_mr_ref_time_ms_threshold: None,
279            auto_mr_ref_input_threshold: None,
280            transparent_policy: default_transparent_policy(),
281        }
282    }
283}
284
285impl PackerConfig {
286    /// Validates the configuration parameters.
287    ///
288    /// Returns an error if:
289    /// - Dimensions are zero or invalid
290    /// - Padding configuration would leave no usable space
291    /// - Other configuration constraints are violated
292    pub fn validate(&self) -> crate::error::Result<()> {
293        use crate::error::TexPackerError;
294
295        // Validate dimensions
296        if self.max_width == 0 || self.max_height == 0 {
297            return Err(TexPackerError::InvalidDimensions {
298                width: self.max_width,
299                height: self.max_height,
300            });
301        }
302
303        // Validate padding doesn't exceed available space
304        let total_border = self.border_padding.saturating_mul(2);
305        let total_padding_per_texture = self
306            .texture_padding
307            .saturating_add(self.texture_extrusion.saturating_mul(2));
308
309        if total_border >= self.max_width || total_border >= self.max_height {
310            return Err(TexPackerError::InvalidConfig(format!(
311                "border_padding ({}) * 2 exceeds atlas dimensions ({}x{})",
312                self.border_padding, self.max_width, self.max_height
313            )));
314        }
315
316        // Check if there's at least 1x1 pixel of usable space after borders
317        let usable_width = self.max_width.saturating_sub(total_border);
318        let usable_height = self.max_height.saturating_sub(total_border);
319
320        if usable_width == 0 || usable_height == 0 {
321            return Err(TexPackerError::InvalidConfig(format!(
322                "No usable space after border_padding: {}x{} - {} * 2 = {}x{}",
323                self.max_width, self.max_height, self.border_padding, usable_width, usable_height
324            )));
325        }
326
327        // Warn if padding per texture is very large relative to atlas size
328        if total_padding_per_texture > usable_width / 2
329            || total_padding_per_texture > usable_height / 2
330        {
331            // This is not an error, but might indicate misconfiguration
332            // We'll allow it but it might result in poor packing
333        }
334
335        // trim_threshold is u8, so it's always valid (0-255)
336
337        Ok(())
338    }
339}
340
341fn default_family() -> AlgorithmFamily {
342    AlgorithmFamily::Skyline
343}
344fn default_mr_heuristic() -> MaxRectsHeuristic {
345    MaxRectsHeuristic::BestAreaFit
346}
347fn default_skyline_heuristic() -> SkylineHeuristic {
348    SkylineHeuristic::BottomLeft
349}
350fn default_g_choice() -> GuillotineChoice {
351    GuillotineChoice::BestAreaFit
352}
353fn default_g_split() -> GuillotineSplit {
354    GuillotineSplit::SplitShorterLeftoverAxis
355}
356fn default_auto_mode() -> AutoMode {
357    AutoMode::Quality
358}
359fn default_sort_order() -> SortOrder {
360    SortOrder::AreaDesc
361}
362fn default_parallel() -> bool {
363    false
364}
365fn default_transparent_policy() -> TransparentPolicy {
366    TransparentPolicy::Keep
367}
368
369/// Builder for `PackerConfig` for ergonomic construction.
370#[derive(Debug, Default, Clone)]
371pub struct PackerConfigBuilder {
372    cfg: PackerConfig,
373}
374
375impl PackerConfigBuilder {
376    pub fn new() -> Self {
377        Self {
378            cfg: PackerConfig::default(),
379        }
380    }
381    pub fn with_max_dimensions(mut self, w: u32, h: u32) -> Self {
382        self.cfg.max_width = w;
383        self.cfg.max_height = h;
384        self
385    }
386    pub fn allow_rotation(mut self, v: bool) -> Self {
387        self.cfg.allow_rotation = v;
388        self
389    }
390    pub fn force_max_dimensions(mut self, v: bool) -> Self {
391        self.cfg.force_max_dimensions = v;
392        self
393    }
394    pub fn border_padding(mut self, v: u32) -> Self {
395        self.cfg.border_padding = v;
396        self
397    }
398    pub fn texture_padding(mut self, v: u32) -> Self {
399        self.cfg.texture_padding = v;
400        self
401    }
402    pub fn texture_extrusion(mut self, v: u32) -> Self {
403        self.cfg.texture_extrusion = v;
404        self
405    }
406    pub fn trim(mut self, v: bool) -> Self {
407        self.cfg.trim = v;
408        self
409    }
410    pub fn trim_threshold(mut self, v: u8) -> Self {
411        self.cfg.trim_threshold = v;
412        self
413    }
414    pub fn outlines(mut self, v: bool) -> Self {
415        self.cfg.texture_outlines = v;
416        self
417    }
418    pub fn pow2(mut self, v: bool) -> Self {
419        self.cfg.power_of_two = v;
420        self
421    }
422    pub fn square(mut self, v: bool) -> Self {
423        self.cfg.square = v;
424        self
425    }
426    pub fn family(mut self, v: AlgorithmFamily) -> Self {
427        self.cfg.family = v;
428        self
429    }
430    pub fn skyline_heuristic(mut self, v: SkylineHeuristic) -> Self {
431        self.cfg.skyline_heuristic = v;
432        self
433    }
434    pub fn mr_heuristic(mut self, v: MaxRectsHeuristic) -> Self {
435        self.cfg.mr_heuristic = v;
436        self
437    }
438    pub fn g_choice(mut self, v: GuillotineChoice) -> Self {
439        self.cfg.g_choice = v;
440        self
441    }
442    pub fn g_split(mut self, v: GuillotineSplit) -> Self {
443        self.cfg.g_split = v;
444        self
445    }
446    pub fn auto_mode(mut self, v: AutoMode) -> Self {
447        self.cfg.auto_mode = v;
448        self
449    }
450    pub fn sort_order(mut self, v: SortOrder) -> Self {
451        self.cfg.sort_order = v;
452        self
453    }
454    pub fn time_budget_ms(mut self, v: Option<u64>) -> Self {
455        self.cfg.time_budget_ms = v;
456        self
457    }
458    pub fn parallel(mut self, v: bool) -> Self {
459        self.cfg.parallel = v;
460        self
461    }
462    pub fn mr_reference(mut self, v: bool) -> Self {
463        self.cfg.mr_reference = v;
464        self
465    }
466    pub fn auto_mr_ref_time_ms_threshold(mut self, v: Option<u64>) -> Self {
467        self.cfg.auto_mr_ref_time_ms_threshold = v;
468        self
469    }
470    pub fn auto_mr_ref_input_threshold(mut self, v: Option<usize>) -> Self {
471        self.cfg.auto_mr_ref_input_threshold = v;
472        self
473    }
474    pub fn use_waste_map(mut self, v: bool) -> Self {
475        self.cfg.use_waste_map = v;
476        self
477    }
478    pub fn transparent_policy(mut self, v: TransparentPolicy) -> Self {
479        self.cfg.transparent_policy = v;
480        self
481    }
482    pub fn build(self) -> PackerConfig {
483        self.cfg
484    }
485}
486
487impl PackerConfig {
488    /// Create a fluent builder for `PackerConfig`.
489    pub fn builder() -> PackerConfigBuilder {
490        PackerConfigBuilder::new()
491    }
492}
493/// Policy for fully transparent images when trimming is enabled and no opaque pixel is found.
494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
495#[serde(rename_all = "snake_case")]
496pub enum TransparentPolicy {
497    /// Keep original dimensions (status quo)
498    Keep,
499    /// Reduce to a 1x1 transparent pixel
500    OneByOne,
501    /// Skip this input entirely
502    Skip,
503}
504
505impl FromStr for TransparentPolicy {
506    type Err = ();
507    fn from_str(s: &str) -> Result<Self, Self::Err> {
508        match s.to_ascii_lowercase().as_str() {
509            "keep" => Ok(Self::Keep),
510            "one_by_one" | "1x1" | "onebyone" => Ok(Self::OneByOne),
511            "skip" => Ok(Self::Skip),
512            _ => Err(()),
513        }
514    }
515}