1use std::str::FromStr;
2use std::time::Duration;
3
4use crate::error::{Result, TexPackerError};
5
6#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}