1use crate::TilerError;
2use crate::config::{DownscalingMethod, TilerConfig};
3use crate::logic::b83;
4use fast_image_resize as fr;
5use image::{Rgb, RgbImage};
6use rayon::prelude::*;
7use std::collections::BTreeSet;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct TileItem {
12 pub level: u32,
13 pub face: char,
14 pub col: u32,
15 pub row: u32,
16 pub image: RgbImage,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct FallbackItem {
22 pub face: char,
23 pub image: RgbImage,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct GeneratedTiles {
29 pub tiles: Vec<TileItem>,
30 pub fallback_tiles: Vec<FallbackItem>,
31 pub missing_tiles_str: Option<String>,
32 pub levels: u32,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
36struct MissingTile {
37 face_idx: usize,
38 level: u32,
39 col: u32,
40 row: u32,
41}
42
43fn is_region_empty(
45 img: &RgbImage,
46 left: u32,
47 upper: u32,
48 width: u32,
49 height: u32,
50 bg_color: Rgb<u8>,
51) -> bool {
52 let stride = img.width() as usize * 3;
53 let raw = img.as_raw();
54 let bg_slice = bg_color.0; for y in upper..(upper + height) {
57 let row_start = (y as usize * stride) + (left as usize * 3);
58 let row_end = row_start + (width as usize * 3);
59 let row_slice = &raw[row_start..row_end];
60
61 if row_slice.chunks_exact(3).any(|pixel| pixel != bg_slice) {
62 return false;
63 }
64 }
65 true
66}
67
68#[allow(clippy::too_many_lines)]
70pub fn generate_pyramid(
71 faces: &[(char, RgbImage)],
72 config: &TilerConfig,
73 clamped_tile_size: u32,
74 actual_cube_size: u32,
75) -> Result<GeneratedTiles, TilerError> {
76 let tile_size = clamped_tile_size.min(actual_cube_size);
77 let face_letters = ['f', 'b', 'u', 'd', 'l', 'r'];
78 let bg_color = Rgb(config.output.background_color);
79
80 let levels = {
81 let ratio = f64::from(actual_cube_size) / f64::from(tile_size);
82 let mut l = (ratio.log2().ceil() as u32) + 1;
83 if l >= 2 && actual_cube_size / 2u32.pow(l - 2) == tile_size {
84 l -= 1; }
86 l
87 };
88
89 let mut level_sizes = vec![0; (levels + 1) as usize];
90 let mut current_size = actual_cube_size;
91 for level in (1..=levels).rev() {
92 level_sizes[level as usize] = current_size;
93 current_size /= 2;
94 }
95
96 let is_partial = config.angles.haov < 360.0 || config.angles.vaov < 180.0;
97
98 let results = faces
100 .par_iter()
101 .enumerate()
102 .map(|(f_idx, &(letter, ref full_face))| {
103 let mut local_tiles = Vec::new();
104 let mut local_missing = Vec::new();
105
106 let mut recursive_face: Option<RgbImage> = None;
107
108 let mut resizer = fr::Resizer::new();
109 let resize_options = fr::ResizeOptions::new()
110 .resize_alg(fr::ResizeAlg::Convolution(fr::FilterType::Lanczos3));
111
112 for level in (1..=levels).rev() {
113 let size = level_sizes[level as usize];
114 let num_tiles_wide_high = (f64::from(size) / f64::from(tile_size)).ceil() as u32;
115 let mut current_face_allocated;
116
117 let active_face: &RgbImage = if level == levels {
118 full_face
119 } else {
120 match config.output.downscaling_method {
121 DownscalingMethod::Recursive => {
122 let mut downscaled = RgbImage::new(size, size);
123 let source = recursive_face.as_ref().unwrap_or(full_face);
124 resizer.resize(source, &mut downscaled, Some(&resize_options))?;
125 recursive_face.insert(downscaled)
126 }
127 DownscalingMethod::Direct => {
128 current_face_allocated = RgbImage::new(size, size);
129 resizer.resize(
130 full_face,
131 &mut current_face_allocated,
132 Some(&resize_options),
133 )?;
134 ¤t_face_allocated
135 }
136 }
137 };
138
139 for row in 0..num_tiles_wide_high {
140 for col in 0..num_tiles_wide_high {
141 let left = col * tile_size;
142 let upper = row * tile_size;
143 let width = tile_size.min(size - left);
144 let height = tile_size.min(size - upper);
145
146 if is_partial
148 && is_region_empty(active_face, left, upper, width, height, bg_color)
149 {
150 local_missing.push(MissingTile {
151 face_idx: f_idx,
152 level,
153 col,
154 row,
155 });
156 } else {
157 let tile_crop =
158 image::imageops::crop_imm(active_face, left, upper, width, height)
159 .to_image();
160 local_tiles.push(TileItem {
161 level,
162 face: letter,
163 col,
164 row,
165 image: tile_crop,
166 });
167 }
168 }
169 }
170 }
171 Ok::<_, TilerError>((local_tiles, local_missing))
172 })
173 .collect::<Result<Vec<_>, TilerError>>();
174 let (tiles_nested, missing_nested): (Vec<Vec<TileItem>>, Vec<Vec<MissingTile>>) =
175 results?.into_iter().unzip();
176
177 let tiles: Vec<TileItem> = tiles_nested.into_iter().flatten().collect();
178 let missing_tiles: Vec<MissingTile> = missing_nested.into_iter().flatten().collect();
179
180 let missing_tiles_str = if missing_tiles.is_empty() {
182 None
183 } else {
184 let mut missing_set: BTreeSet<MissingTile> = missing_tiles.into_iter().collect();
185
186 let mut redundant = Vec::new();
188 for &t in &missing_set {
189 if t.level > 1 {
190 let parent = MissingTile {
191 face_idx: t.face_idx,
192 level: t.level - 1,
193 col: t.col / 2,
194 row: t.row / 2,
195 };
196 if missing_set.contains(&parent) {
197 redundant.push(t);
198 }
199 }
200 }
201 for r in redundant {
202 missing_set.remove(&r);
203 }
204
205 let mut sorted_missing: Vec<MissingTile> = missing_set.into_iter().collect();
207 sorted_missing.sort();
208
209 let mut missing_str = String::new();
210 let mut prev_face: Option<usize> = None;
211 let mut prev_level: Option<u32> = None;
212 let mut num_tile_digits = 1;
213
214 for mt in sorted_missing {
215 if Some(mt.face_idx) != prev_face {
216 missing_str.push('!');
217 missing_str.push(face_letters[mt.face_idx]);
218 prev_level = None;
219 }
220 if Some(mt.level) != prev_level {
221 missing_str.push('>');
222 missing_str.push_str(&b83::encode(&[mt.level], 1));
223 let level_size = level_sizes[mt.level as usize];
224 let max_tile_num = (f64::from(level_size) / f64::from(tile_size)).ceil() as u32 - 1;
225 num_tile_digits = (f64::from(max_tile_num + 1).log(83.0).ceil() as usize).max(1);
226 }
227 missing_str.push_str(&b83::encode(&[mt.col, mt.row], num_tile_digits));
228 prev_face = Some(mt.face_idx);
229 prev_level = Some(mt.level);
230 }
231 Some(missing_str)
232 };
233
234 let mut fallback_tiles = Vec::new();
236 if config.output.fallback_size > 0 {
237 let mut resizer = fr::Resizer::new();
238 let resize_options = fr::ResizeOptions::new()
239 .resize_alg(fr::ResizeAlg::Convolution(fr::FilterType::Lanczos3));
240
241 for &(letter, ref full_face) in faces {
242 let mut resized_face =
243 RgbImage::new(config.output.fallback_size, config.output.fallback_size);
244 resizer.resize(full_face, &mut resized_face, Some(&resize_options))?;
245
246 fallback_tiles.push(FallbackItem {
247 face: letter,
248 image: resized_face,
249 });
250 }
251 }
252
253 Ok(GeneratedTiles {
254 tiles,
255 fallback_tiles,
256 missing_tiles_str,
257 levels,
258 })
259}