tex_packer_core/
runtime.rs

1use crate::config::{GuillotineChoice, GuillotineSplit, PackerConfig, SkylineHeuristic};
2use crate::error::{Result, TexPackerError};
3use crate::model::{Atlas, Frame, Meta, Page, Rect};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
7pub enum RuntimeStrategy {
8    Guillotine,
9    Shelf(ShelfPolicy),
10    Skyline(SkylineHeuristic),
11}
12
13#[derive(Debug, Clone, Copy)]
14pub enum ShelfPolicy {
15    NextFit,
16    FirstFit,
17}
18
19/// Runtime statistics for an atlas session.
20#[derive(Debug, Clone)]
21pub struct RuntimeStats {
22    /// Number of pages currently allocated.
23    pub num_pages: usize,
24    /// Number of textures currently in the atlas.
25    pub num_textures: usize,
26    /// Total area of all pages (width * height * num_pages).
27    pub total_page_area: u64,
28    /// Total area occupied by textures (sum of all used slots).
29    pub total_used_area: u64,
30    /// Total free area available for new textures.
31    pub total_free_area: u64,
32    /// Occupancy ratio: used_area / total_page_area (0.0 to 1.0).
33    pub occupancy: f64,
34    /// Number of free rectangles/segments (fragmentation indicator).
35    pub num_free_rects: usize,
36}
37
38impl RuntimeStats {
39    /// Returns a human-readable summary of the statistics.
40    pub fn summary(&self) -> String {
41        format!(
42            "Pages: {}, Textures: {}, Occupancy: {:.2}%, Free: {} px² ({} rects), Used: {} px²",
43            self.num_pages,
44            self.num_textures,
45            self.occupancy * 100.0,
46            self.total_free_area,
47            self.num_free_rects,
48            self.total_used_area,
49        )
50    }
51
52    /// Returns the fragmentation ratio (higher = more fragmented).
53    /// Calculated as: num_free_rects / (total_free_area / 1000)
54    /// Lower is better (fewer, larger free blocks).
55    pub fn fragmentation(&self) -> f64 {
56        if self.total_free_area == 0 {
57            0.0
58        } else {
59            (self.num_free_rects as f64) / ((self.total_free_area as f64) / 1000.0).max(1.0)
60        }
61    }
62
63    /// Returns the waste percentage (0.0 to 100.0).
64    pub fn waste_percentage(&self) -> f64 {
65        if self.total_page_area > 0 {
66            (self.total_free_area as f64 / self.total_page_area as f64) * 100.0
67        } else {
68            0.0
69        }
70    }
71}
72
73pub struct AtlasSession {
74    pub(crate) cfg: PackerConfig,
75    _strategy: RuntimeStrategy,
76    pages: Vec<RtPage>,
77    next_id: usize,
78}
79
80struct RtPage {
81    id: usize,
82    width: u32,
83    height: u32,
84    // Used map of reserved slots (expanded by padding/extrude)
85    used: HashMap<String, (Rect, bool, Frame<String>)>, // (reserved_slot, rotated, frame)
86    allow_rotation: bool,
87    mode: RtMode,
88}
89
90enum RtMode {
91    Guillotine {
92        free: Vec<Rect>,
93        choice: GuillotineChoice,
94        split: GuillotineSplit,
95    },
96    Shelf {
97        border: Rect,
98        policy: ShelfPolicy,
99        shelves: Vec<Shelf>,
100        next_y: u32,
101    },
102    Skyline {
103        border: Rect,
104        heuristic: SkylineHeuristic,
105        skylines: Vec<SkylineNode>,
106    },
107}
108
109#[derive(Clone, Debug)]
110struct Shelf {
111    y: u32,
112    h: u32,
113    segs: Vec<(u32, u32)>,
114}
115
116#[derive(Clone, Copy, Debug)]
117struct SkylineNode {
118    x: u32,
119    y: u32,
120    w: u32,
121}
122
123impl AtlasSession {
124    pub fn new(cfg: PackerConfig, strategy: RuntimeStrategy) -> Self {
125        Self {
126            cfg,
127            _strategy: strategy,
128            pages: Vec::new(),
129            next_id: 0,
130        }
131    }
132
133    fn new_page(&mut self) -> RtPage {
134        let id = self.next_id;
135        self.next_id += 1;
136        let pad = self.cfg.border_padding;
137        let w = self.cfg.max_width.saturating_sub(pad.saturating_mul(2));
138        let h = self.cfg.max_height.saturating_sub(pad.saturating_mul(2));
139        let mode = match &self._strategy {
140            RuntimeStrategy::Guillotine => RtMode::Guillotine {
141                free: vec![Rect::new(pad, pad, w, h)],
142                choice: self.cfg.g_choice.clone(),
143                split: self.cfg.g_split.clone(),
144            },
145            RuntimeStrategy::Shelf(policy) => RtMode::Shelf {
146                border: Rect::new(pad, pad, w, h),
147                policy: *policy,
148                shelves: Vec::new(),
149                next_y: pad,
150            },
151            RuntimeStrategy::Skyline(heuristic) => RtMode::Skyline {
152                border: Rect::new(pad, pad, w, h),
153                heuristic: heuristic.clone(),
154                skylines: vec![SkylineNode { x: pad, y: pad, w }],
155            },
156        };
157        RtPage {
158            id,
159            width: self.cfg.max_width,
160            height: self.cfg.max_height,
161            used: HashMap::new(),
162            allow_rotation: self.cfg.allow_rotation,
163            mode,
164        }
165    }
166
167    pub fn append(&mut self, key: String, w: u32, h: u32) -> Result<(usize, Frame<String>)> {
168        let reserve_w = w + self.cfg.texture_extrusion * 2 + self.cfg.texture_padding;
169        let reserve_h = h + self.cfg.texture_extrusion * 2 + self.cfg.texture_padding;
170        // Try existing pages
171        for idx in 0..self.pages.len() {
172            let (slot, rotated, id);
173            {
174                let p = &self.pages[idx];
175                if let Some((s, r)) = p.choose(reserve_w, reserve_h) {
176                    slot = s;
177                    rotated = r;
178                    id = p.id;
179                } else {
180                    continue;
181                }
182            }
183            let frame = self.make_frame(&key, w, h, &slot, rotated);
184            let p = &mut self.pages[idx];
185            p.place(&key, &slot, &frame, rotated);
186            return Ok((id, frame));
187        }
188        // Grow: add a new page and place
189        let mut page = self.new_page();
190        if let Some((slot, rotated)) = page.choose(reserve_w, reserve_h) {
191            let frame = self.make_frame(&key, w, h, &slot, rotated);
192            page.place(&key, &slot, &frame, rotated);
193            let id = page.id;
194            self.pages.push(page);
195            return Ok((id, frame));
196        }
197        Err(TexPackerError::OutOfSpace {
198            key,
199            width: w,
200            height: h,
201            pages_attempted: self.pages.len() + 1,
202        })
203    }
204
205    pub fn evict(&mut self, page_id: usize, key: &str) -> bool {
206        if let Some(p) = self.pages.iter_mut().find(|p| p.id == page_id) {
207            if let Some((slot, _rot, _frame)) = p.used.remove(key) {
208                p.add_free(slot);
209                return true;
210            }
211        }
212        false
213    }
214
215    pub fn snapshot_atlas(&self) -> Atlas<String> {
216        let mut pages: Vec<Page<String>> = Vec::new();
217        for p in &self.pages {
218            let mut frames: Vec<Frame<String>> = Vec::new();
219            for (_k, (_slot, _rot, f)) in p.used.iter() {
220                frames.push(f.clone());
221            }
222            pages.push(Page {
223                id: p.id,
224                width: p.width,
225                height: p.height,
226                frames,
227            });
228        }
229        let meta = Meta {
230            schema_version: "1".into(),
231            app: "tex-packer".into(),
232            version: env!("CARGO_PKG_VERSION").into(),
233            format: "RGBA8888".into(),
234            scale: 1.0,
235            power_of_two: self.cfg.power_of_two,
236            square: self.cfg.square,
237            max_dim: (self.cfg.max_width, self.cfg.max_height),
238            padding: (self.cfg.border_padding, self.cfg.texture_padding),
239            extrude: self.cfg.texture_extrusion,
240            allow_rotation: self.cfg.allow_rotation,
241            trim_mode: if self.cfg.trim { "trim" } else { "none" }.into(),
242            background_color: None,
243        };
244        Atlas { pages, meta }
245    }
246
247    /// Find a frame by its key.
248    /// Returns the page ID and a reference to the frame if found.
249    pub fn get_frame(&self, key: &str) -> Option<(usize, &Frame<String>)> {
250        for page in &self.pages {
251            if let Some((_slot, _rot, frame)) = page.used.get(key) {
252                return Some((page.id, frame));
253            }
254        }
255        None
256    }
257
258    /// Find the reserved slot rectangle for a texture key (pre-offset area including padding/extrude).
259    pub fn get_reserved_slot(&self, key: &str) -> Option<(usize, Rect)> {
260        for page in &self.pages {
261            if let Some((slot, _rot, _frame)) = page.used.get(key) {
262                return Some((page.id, *slot));
263            }
264        }
265        None
266    }
267
268    /// Evict a texture by its key without needing to know the page ID.
269    /// Returns true if the texture was found and evicted.
270    pub fn evict_by_key(&mut self, key: &str) -> bool {
271        for page in &mut self.pages {
272            if let Some((slot, _rot, _frame)) = page.used.remove(key) {
273                page.add_free(slot);
274                return true;
275            }
276        }
277        false
278    }
279
280    /// Check if a texture with the given key exists.
281    pub fn contains(&self, key: &str) -> bool {
282        self.pages.iter().any(|p| p.used.contains_key(key))
283    }
284
285    /// Get all texture keys currently in the atlas.
286    pub fn keys(&self) -> Vec<&str> {
287        self.pages
288            .iter()
289            .flat_map(|p| p.used.keys().map(|k| k.as_str()))
290            .collect()
291    }
292
293    /// Get the number of textures currently in the atlas.
294    pub fn texture_count(&self) -> usize {
295        self.pages.iter().map(|p| p.used.len()).sum()
296    }
297
298    /// Get runtime statistics about the atlas session.
299    pub fn stats(&self) -> RuntimeStats {
300        let num_pages = self.pages.len();
301        let num_textures = self.texture_count();
302
303        let mut total_used_area = 0u64;
304        let mut total_free_area = 0u64;
305        let mut num_free_rects = 0;
306
307        for page in &self.pages {
308            // Calculate used area
309            for (slot, _rot, _frame) in page.used.values() {
310                total_used_area += (slot.w as u64) * (slot.h as u64);
311            }
312
313            // Calculate free area
314            match &page.mode {
315                RtMode::Guillotine { free, .. } => {
316                    num_free_rects += free.len();
317                    for rect in free {
318                        total_free_area += (rect.w as u64) * (rect.h as u64);
319                    }
320                }
321                RtMode::Shelf { shelves, .. } => {
322                    for shelf in shelves {
323                        num_free_rects += shelf.segs.len();
324                        for (_, w) in &shelf.segs {
325                            total_free_area += (*w as u64) * (shelf.h as u64);
326                        }
327                    }
328                }
329                RtMode::Skyline {
330                    border, skylines, ..
331                } => {
332                    // Approximate free area as the area above skyline using exclusive bottom
333                    num_free_rects += skylines.len();
334                    let bottom_ex = border.y + border.h; // exclusive bottom
335                    for node in skylines {
336                        let height_above = bottom_ex.saturating_sub(node.y);
337                        total_free_area += (node.w as u64) * (height_above as u64);
338                    }
339                }
340            }
341        }
342
343        let total_page_area = if num_pages > 0 {
344            (self.cfg.max_width as u64) * (self.cfg.max_height as u64) * (num_pages as u64)
345        } else {
346            0
347        };
348
349        let occupancy = if total_page_area > 0 {
350            total_used_area as f64 / total_page_area as f64
351        } else {
352            0.0
353        };
354
355        RuntimeStats {
356            num_pages,
357            num_textures,
358            total_page_area,
359            total_used_area,
360            total_free_area,
361            occupancy,
362            num_free_rects,
363        }
364    }
365
366    fn make_frame(&self, key: &str, w: u32, h: u32, slot: &Rect, rotated: bool) -> Frame<String> {
367        let pad_half = self.cfg.texture_padding / 2;
368        let off = self.cfg.texture_extrusion + pad_half;
369        let (fw, fh) = (w, h);
370        let frame = Rect::new(slot.x + off, slot.y + off, fw, fh);
371        let source = Rect::new(0, 0, w, h);
372        Frame {
373            key: key.to_string(),
374            frame,
375            rotated,
376            trimmed: false,
377            source,
378            source_size: (w, h),
379        }
380    }
381}
382
383impl RtPage {
384    fn choose(&self, w: u32, h: u32) -> Option<(Rect, bool)> {
385        match &self.mode {
386            RtMode::Guillotine { free, choice, .. } => {
387                let mut best_idx = None;
388                let mut best = Rect::new(0, 0, 0, 0);
389                let mut best_s = i32::MAX;
390                let mut best_s2 = i32::MAX;
391                let mut best_rot = false;
392                for (i, fr) in free.iter().enumerate() {
393                    if fr.w >= w && fr.h >= h {
394                        let (s1, s2) = score_choice(choice, fr, w, h);
395                        if s1 < best_s || (s1 == best_s && s2 < best_s2) {
396                            best_s = s1;
397                            best_s2 = s2;
398                            best_idx = Some(i);
399                            best = Rect::new(fr.x, fr.y, w, h);
400                            best_rot = false;
401                        }
402                    }
403                    if self.allow_rotation && fr.w >= h && fr.h >= w {
404                        let (s1, s2) = score_choice(choice, fr, h, w);
405                        if s1 < best_s || (s1 == best_s && s2 < best_s2) {
406                            best_s = s1;
407                            best_s2 = s2;
408                            best_idx = Some(i);
409                            best = Rect::new(fr.x, fr.y, h, w);
410                            best_rot = true;
411                        }
412                    }
413                }
414                best_idx.map(|_| (best, best_rot))
415            }
416            RtMode::Shelf {
417                border,
418                policy,
419                shelves,
420                next_y,
421            } => choose_shelf(self.allow_rotation, border, *policy, shelves, *next_y, w, h),
422            RtMode::Skyline {
423                border,
424                heuristic,
425                skylines,
426            } => choose_skyline(self.allow_rotation, border, heuristic, skylines, w, h),
427        }
428    }
429
430    fn place(&mut self, key: &str, slot: &Rect, frame: &Frame<String>, rotated: bool) {
431        match &mut self.mode {
432            RtMode::Guillotine { free, split, .. } => {
433                // remove chosen free and split
434                let mut idx = None;
435                for (i, fr) in free.iter().enumerate() {
436                    if fr.x == slot.x && fr.y == slot.y && fr.w >= slot.w && fr.h >= slot.h {
437                        idx = Some(i);
438                        break;
439                    }
440                }
441                if let Some(i) = idx {
442                    // emulate original split on matched free[i]
443                    let fr = free[i];
444                    free.swap_remove(i);
445                    let (a, b) = split_rect(split, &fr, slot);
446                    if let Some(r) = a {
447                        free.push(r);
448                    }
449                    if let Some(r) = b {
450                        free.push(r);
451                    }
452                    prune_free_list(free);
453                    merge_free_list(free);
454                }
455            }
456            RtMode::Shelf {
457                border,
458                shelves,
459                next_y,
460                ..
461            } => {
462                // consume from shelf at slot.y, or create new shelf and consume
463                if let Some(sh) = shelves.iter_mut().find(|s| s.y == slot.y && s.h >= slot.h) {
464                    consume_from_shelf(sh, slot, border);
465                } else {
466                    let mut sh = Shelf {
467                        y: slot.y,
468                        h: slot.h,
469                        segs: vec![(border.x, border.w)],
470                    };
471                    consume_from_shelf(&mut sh, slot, border);
472                    shelves.push(sh);
473                    *next_y = (*next_y).max(slot.y + slot.h);
474                }
475            }
476            RtMode::Skyline { skylines, .. } => {
477                place_skyline(skylines, slot);
478            }
479        }
480        self.used
481            .insert(key.to_string(), (*slot, rotated, frame.clone()));
482    }
483
484    fn add_free(&mut self, r: Rect) {
485        match &mut self.mode {
486            RtMode::Guillotine { free, .. } => {
487                free.push(r);
488                prune_free_list(free);
489                merge_free_list(free);
490            }
491            RtMode::Shelf { shelves, .. } => {
492                if let Some(sh) = shelves.iter_mut().find(|s| s.y == r.y && s.h == r.h) {
493                    sh.segs.push((r.x, r.w));
494                    merge_shelf_segments(sh);
495                } else {
496                    shelves.push(Shelf {
497                        y: r.y,
498                        h: r.h,
499                        segs: vec![(r.x, r.w)],
500                    });
501                }
502            }
503            RtMode::Skyline { .. } => {
504                // Skyline doesn't support add_free (eviction not optimized)
505            }
506        }
507    }
508
509    // guillotine prune/split helpers moved to free functions below
510}
511
512fn score_choice(choice: &GuillotineChoice, fr: &Rect, w: u32, h: u32) -> (i32, i32) {
513    let area_fit = (fr.w * fr.h) as i32 - (w * h) as i32;
514    let leftover_h = fr.w as i32 - w as i32;
515    let leftover_v = fr.h as i32 - h as i32;
516    let short_fit = leftover_h.abs().min(leftover_v.abs());
517    let long_fit = leftover_h.abs().max(leftover_v.abs());
518    match choice {
519        GuillotineChoice::BestAreaFit => (area_fit, short_fit),
520        GuillotineChoice::BestShortSideFit => (short_fit, long_fit),
521        GuillotineChoice::BestLongSideFit => (long_fit, short_fit),
522        GuillotineChoice::WorstAreaFit => (-area_fit, -short_fit),
523        GuillotineChoice::WorstShortSideFit => (-short_fit, -long_fit),
524        GuillotineChoice::WorstLongSideFit => (-long_fit, -short_fit),
525    }
526}
527
528fn split_rect(split: &GuillotineSplit, fr: &Rect, placed: &Rect) -> (Option<Rect>, Option<Rect>) {
529    let w_right = (fr.x + fr.w).saturating_sub(placed.x + placed.w);
530    let h_bottom = (fr.y + fr.h).saturating_sub(placed.y + placed.h);
531    let split_horizontal = match split {
532        GuillotineSplit::SplitShorterLeftoverAxis => h_bottom < w_right,
533        GuillotineSplit::SplitLongerLeftoverAxis => h_bottom > w_right,
534        GuillotineSplit::SplitMinimizeArea => (w_right * fr.h) <= (fr.w * h_bottom),
535        GuillotineSplit::SplitMaximizeArea => (w_right * fr.h) >= (fr.w * h_bottom),
536        GuillotineSplit::SplitShorterAxis => fr.h < fr.w,
537        GuillotineSplit::SplitLongerAxis => fr.h > fr.w,
538    };
539    let mut bottom = Rect::new(fr.x, placed.y + placed.h, 0, fr.h.saturating_sub(placed.h));
540    let mut right = Rect::new(placed.x + placed.w, fr.y, fr.w.saturating_sub(placed.w), 0);
541    if split_horizontal {
542        bottom.w = fr.w;
543        right.h = placed.h;
544    } else {
545        bottom.w = placed.w;
546        right.h = fr.h;
547    }
548    (
549        if bottom.w > 0 && bottom.h > 0 {
550            Some(bottom)
551        } else {
552            None
553        },
554        if right.w > 0 && right.h > 0 {
555            Some(right)
556        } else {
557            None
558        },
559    )
560}
561
562// ---------- helpers for page modes ----------
563
564fn prune_free_list(free: &mut Vec<Rect>) {
565    let mut i = 0;
566    while i < free.len() {
567        let mut j = i + 1;
568        let a = free[i];
569        let a_x2 = a.x + a.w;
570        let a_y2 = a.y + a.h;
571        let mut remove_i = false;
572        while j < free.len() {
573            let b = free[j];
574            let b_x2 = b.x + b.w;
575            let b_y2 = b.y + b.h;
576            if a.x >= b.x && a.y >= b.y && a_x2 <= b_x2 && a_y2 <= b_y2 {
577                remove_i = true;
578                break;
579            }
580            if b.x >= a.x && b.y >= a.y && b_x2 <= a_x2 && b_y2 <= a_y2 {
581                free.remove(j);
582                continue;
583            }
584            j += 1;
585        }
586        if remove_i {
587            free.remove(i);
588        } else {
589            i += 1;
590        }
591    }
592}
593
594fn merge_free_list(free: &mut Vec<Rect>) {
595    let mut merged = true;
596    while merged {
597        merged = false;
598        'outer: for i in 0..free.len() {
599            for j in i + 1..free.len() {
600                let a = free[i];
601                let b = free[j];
602                if a.y == b.y && a.h == b.h {
603                    if a.x + a.w == b.x {
604                        free[i] = Rect::new(a.x, a.y, a.w + b.w, a.h);
605                        free.remove(j);
606                        merged = true;
607                        break 'outer;
608                    } else if b.x + b.w == a.x {
609                        free[i] = Rect::new(b.x, a.y, a.w + b.w, a.h);
610                        free.remove(j);
611                        merged = true;
612                        break 'outer;
613                    }
614                }
615                if a.x == b.x && a.w == b.w {
616                    if a.y + a.h == b.y {
617                        free[i] = Rect::new(a.x, a.y, a.w, a.h + b.h);
618                        free.remove(j);
619                        merged = true;
620                        break 'outer;
621                    } else if b.y + b.h == a.y {
622                        free[i] = Rect::new(a.x, b.y, a.w, a.h + b.h);
623                        free.remove(j);
624                        merged = true;
625                        break 'outer;
626                    }
627                }
628            }
629        }
630    }
631}
632
633fn choose_shelf(
634    allow_rot: bool,
635    border: &Rect,
636    policy: ShelfPolicy,
637    shelves: &Vec<Shelf>,
638    next_y: u32,
639    w: u32,
640    h: u32,
641) -> Option<(Rect, bool)> {
642    let try_in = |rw: u32, rh: u32| -> Option<Rect> {
643        match policy {
644            ShelfPolicy::FirstFit => {
645                for sh in shelves {
646                    if rh <= sh.h {
647                        if let Some((sx, _sw)) = sh
648                            .segs
649                            .iter()
650                            .find(|(sx, sw)| *sw >= rw && *sx + rw <= border.x + border.w)
651                        {
652                            return Some(Rect::new(*sx, sh.y, rw, rh));
653                        }
654                    }
655                }
656                None
657            }
658            ShelfPolicy::NextFit => {
659                if let Some(sh) = shelves.last() {
660                    if rh <= sh.h {
661                        if let Some((sx, _sw)) = sh
662                            .segs
663                            .iter()
664                            .find(|(sx, sw)| *sw >= rw && *sx + rw <= border.x + border.w)
665                        {
666                            return Some(Rect::new(*sx, sh.y, rw, rh));
667                        }
668                    }
669                }
670                None
671            }
672        }
673    };
674    if let Some(r) = try_in(w, h) {
675        return Some((r, false));
676    }
677    if allow_rot {
678        if let Some(r) = try_in(h, w) {
679            return Some((r, true));
680        }
681    }
682    let try_new = |rw: u32, rh: u32| -> Option<Rect> {
683        if rw <= border.w && next_y + rh <= border.y + border.h {
684            Some(Rect::new(border.x, next_y, rw, rh))
685        } else {
686            None
687        }
688    };
689    if let Some(r) = try_new(w, h) {
690        return Some((r, false));
691    }
692    if allow_rot {
693        if let Some(r) = try_new(h, w) {
694            return Some((r, true));
695        }
696    }
697    None
698}
699
700fn consume_from_shelf(sh: &mut Shelf, slot: &Rect, border: &Rect) {
701    let mut i = 0;
702    while i < sh.segs.len() {
703        let (sx, sw) = sh.segs[i];
704        if slot.x >= sx && slot.x + slot.w <= sx + sw {
705            sh.segs.remove(i);
706            let left_w = slot.x.saturating_sub(sx);
707            let right_x = slot.x + slot.w;
708            let right_w = (sx + sw).saturating_sub(right_x);
709            if left_w > 0 {
710                sh.segs.push((sx, left_w));
711            }
712            if right_w > 0 {
713                sh.segs.push((right_x, right_w));
714            }
715            break;
716        } else {
717            i += 1;
718        }
719    }
720    merge_shelf_segments(sh);
721    sh.segs
722        .retain(|(x, w)| *w > 0 && *x >= border.x && *x + *w <= border.x + border.w);
723}
724
725fn merge_shelf_segments(sh: &mut Shelf) {
726    sh.segs.sort_by_key(|(x, _)| *x);
727    let mut out: Vec<(u32, u32)> = Vec::new();
728    for (x, w) in sh.segs.drain(..) {
729        if let Some((lx, lw)) = out.last_mut() {
730            if *lx + *lw == x {
731                *lw += w;
732                continue;
733            }
734        }
735        out.push((x, w));
736    }
737    sh.segs = out;
738}
739
740// Skyline helper functions
741fn choose_skyline(
742    allow_rotation: bool,
743    border: &Rect,
744    heuristic: &SkylineHeuristic,
745    skylines: &[SkylineNode],
746    w: u32,
747    h: u32,
748) -> Option<(Rect, bool)> {
749    match heuristic {
750        SkylineHeuristic::BottomLeft => {
751            find_skyline_bottom_left(allow_rotation, border, skylines, w, h)
752        }
753        SkylineHeuristic::MinWaste => {
754            find_skyline_min_waste(allow_rotation, border, skylines, w, h)
755        }
756    }
757}
758
759fn can_put_skyline(
760    skylines: &[SkylineNode],
761    border: &Rect,
762    mut i: usize,
763    w: u32,
764    h: u32,
765) -> Option<Rect> {
766    if i >= skylines.len() {
767        return None;
768    }
769    let mut rect = Rect::new(skylines[i].x, 0, w, h);
770    let mut width_left = rect.w;
771    loop {
772        rect.y = rect.y.max(skylines[i].y);
773        if !border.contains(&rect) {
774            return None;
775        }
776        if skylines[i].w >= width_left {
777            return Some(rect);
778        }
779        width_left = width_left.saturating_sub(skylines[i].w);
780        i += 1;
781        if i >= skylines.len() {
782            return None;
783        }
784    }
785}
786
787fn find_skyline_bottom_left(
788    allow_rotation: bool,
789    border: &Rect,
790    skylines: &[SkylineNode],
791    w: u32,
792    h: u32,
793) -> Option<(Rect, bool)> {
794    let mut best_bottom = u32::MAX;
795    let mut best_width = u32::MAX;
796    let mut best_index: Option<usize> = None;
797    let mut best_rect = Rect::new(0, 0, 0, 0);
798    let mut best_rot = false;
799
800    for i in 0..skylines.len() {
801        if let Some(r) = can_put_skyline(skylines, border, i, w, h) {
802            if r.bottom() < best_bottom || (r.bottom() == best_bottom && skylines[i].w < best_width)
803            {
804                best_bottom = r.bottom();
805                best_width = skylines[i].w;
806                best_index = Some(i);
807                best_rect = r;
808                best_rot = false;
809            }
810        }
811        if allow_rotation {
812            if let Some(r) = can_put_skyline(skylines, border, i, h, w) {
813                if r.bottom() < best_bottom
814                    || (r.bottom() == best_bottom && skylines[i].w < best_width)
815                {
816                    best_bottom = r.bottom();
817                    best_width = skylines[i].w;
818                    best_index = Some(i);
819                    best_rect = r;
820                    best_rot = true;
821                }
822            }
823        }
824    }
825
826    best_index.map(|_| (best_rect, best_rot))
827}
828
829fn find_skyline_min_waste(
830    allow_rotation: bool,
831    border: &Rect,
832    skylines: &[SkylineNode],
833    w: u32,
834    h: u32,
835) -> Option<(Rect, bool)> {
836    let mut best_waste = i64::MAX;
837    let mut best_bottom = u32::MAX;
838    let mut best_index: Option<usize> = None;
839    let mut best_rect = Rect::new(0, 0, 0, 0);
840    let mut best_rot = false;
841
842    for i in 0..skylines.len() {
843        if let Some(r) = can_put_skyline(skylines, border, i, w, h) {
844            let waste = compute_waste(skylines, i, &r);
845            if waste < best_waste || (waste == best_waste && r.bottom() < best_bottom) {
846                best_waste = waste;
847                best_bottom = r.bottom();
848                best_index = Some(i);
849                best_rect = r;
850                best_rot = false;
851            }
852        }
853        if allow_rotation {
854            if let Some(r) = can_put_skyline(skylines, border, i, h, w) {
855                let waste = compute_waste(skylines, i, &r);
856                if waste < best_waste || (waste == best_waste && r.bottom() < best_bottom) {
857                    best_waste = waste;
858                    best_bottom = r.bottom();
859                    best_index = Some(i);
860                    best_rect = r;
861                    best_rot = true;
862                }
863            }
864        }
865    }
866
867    best_index.map(|_| (best_rect, best_rot))
868}
869
870fn compute_waste(skylines: &[SkylineNode], start_idx: usize, rect: &Rect) -> i64 {
871    let mut waste = 0i64;
872    let rect_right = rect.x + rect.w;
873    let mut i = start_idx;
874    while i < skylines.len() && skylines[i].x < rect_right {
875        if skylines[i].y < rect.bottom() {
876            let overlap_w = rect_right
877                .min(skylines[i].x + skylines[i].w)
878                .saturating_sub(skylines[i].x.max(rect.x));
879            let overlap_h = rect.bottom().saturating_sub(skylines[i].y);
880            waste += (overlap_w as i64) * (overlap_h as i64);
881        }
882        i += 1;
883    }
884    waste
885}
886
887fn place_skyline(skylines: &mut Vec<SkylineNode>, slot: &Rect) {
888    // Find nodes that intersect with the placed rectangle
889    let mut first_idx = None;
890    let mut last_idx = None;
891
892    for (i, node) in skylines.iter().enumerate() {
893        if node.x < slot.x + slot.w && node.x + node.w > slot.x {
894            if first_idx.is_none() {
895                first_idx = Some(i);
896            }
897            last_idx = Some(i);
898        }
899    }
900
901    if let (Some(first), Some(last)) = (first_idx, last_idx) {
902        // Create new node at the placed rectangle's top
903        let new_node = SkylineNode {
904            x: slot.x,
905            y: slot.bottom(),
906            w: slot.w,
907        };
908
909        // Remove intersecting nodes and insert new node
910        skylines.drain(first..=last);
911        skylines.insert(first, new_node);
912
913        // Merge adjacent nodes with same height
914        merge_skyline_nodes(skylines);
915    }
916}
917
918fn merge_skyline_nodes(skylines: &mut Vec<SkylineNode>) {
919    let mut i = 0;
920    while i < skylines.len().saturating_sub(1) {
921        if skylines[i].y == skylines[i + 1].y {
922            skylines[i].w += skylines[i + 1].w;
923            skylines.remove(i + 1);
924        } else {
925            i += 1;
926        }
927    }
928}