1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4use fastpack_core::{
5 algorithms::{
6 basic::Basic,
7 grid::Grid,
8 maxrects::MaxRects,
9 packer::{PackInput, Packer},
10 },
11 imaging::{alias::detect_aliases, extrude, loader, trim},
12 types::{
13 atlas::AtlasFrame,
14 config::{AlgorithmConfig, Project},
15 rect::{Rect, SourceRect},
16 sprite::Sprite,
17 },
18};
19use rayon::prelude::*;
20use walkdir::WalkDir;
21
22pub struct FrameInfo {
24 pub id: String,
26 pub x: u32,
28 pub y: u32,
30 pub w: u32,
32 pub h: u32,
34 pub alias_of: Option<String>,
36}
37
38pub struct SheetOutput {
40 pub rgba: Vec<u8>,
42 pub width: u32,
44 pub height: u32,
46 pub frames: Vec<FrameInfo>,
48 pub atlas_frames: Vec<AtlasFrame>,
50}
51
52pub struct WorkerOutput {
54 pub sheets: Vec<SheetOutput>,
56 pub sprite_count: usize,
58 pub alias_count: usize,
60 pub overflow_count: usize,
62}
63
64pub enum WorkerMessage {
66 Started,
68 Progress { done: usize, total: usize },
70 Finished(Box<WorkerOutput>),
72 Failed(String),
74}
75
76static IMAGE_EXTENSIONS: &[&str] = &[
77 "png", "jpg", "jpeg", "bmp", "tga", "webp", "tiff", "tif", "gif",
78];
79
80fn is_image(path: &Path) -> bool {
81 path.extension()
82 .and_then(|e| e.to_str())
83 .map(|e| IMAGE_EXTENSIONS.contains(&e.to_lowercase().as_str()))
84 .unwrap_or(false)
85}
86
87fn file_id(path: &Path, base: &Path) -> String {
88 let rel = path.strip_prefix(base).unwrap_or(path);
89 rel.with_extension("").to_string_lossy().replace('\\', "/")
90}
91
92fn collect_images(project: &Project) -> Vec<(PathBuf, String)> {
93 let mut paths = Vec::new();
94 for source in &project.sources {
95 if source.path.is_file() {
96 if is_image(&source.path) {
97 let base = source.path.parent().unwrap_or(Path::new(""));
98 paths.push((source.path.clone(), file_id(&source.path, base)));
99 }
100 } else {
101 for entry in WalkDir::new(&source.path)
102 .sort_by_file_name()
103 .into_iter()
104 .flatten()
105 {
106 if entry.file_type().is_file() && is_image(entry.path()) {
107 let id = file_id(entry.path(), &source.path);
108 paths.push((entry.path().to_path_buf(), id));
109 }
110 }
111 }
112 }
113 paths
114}
115
116fn build_sheet(
117 packer: &dyn Packer,
118 sprites: Vec<Sprite>,
119 project: &Project,
120) -> Result<(SheetOutput, Vec<Sprite>)> {
121 let sprite_cfg = &project.config.sprites;
122 let pack_output = packer
123 .pack(PackInput {
124 sprites,
125 config: project.config.layout.clone(),
126 sprite_config: sprite_cfg.clone(),
127 })
128 .map_err(|e| anyhow::anyhow!("packing failed: {e}"))?;
129
130 let overflow = pack_output.overflow;
131
132 let aw = pack_output.atlas_size.w as usize;
135 let ah = pack_output.atlas_size.h as usize;
136 let mut canvas_raw = vec![0u8; aw * ah * 4];
137 let buf_ptr = canvas_raw.as_mut_ptr() as usize;
138 let buf_stride = aw;
139
140 pack_output.placed.par_iter().for_each(move |ps| {
141 let dx = ps.placement.dest.x as usize;
142 let dy = ps.placement.dest.y as usize;
143 let dw = ps.placement.dest.w as usize;
144 let dh = ps.placement.dest.h as usize;
145 let rgba = ps.sprite.image.as_rgba8().expect("sprite is rgba8");
146 let dst = buf_ptr as *mut u8;
147
148 if ps.placement.rotated {
149 let rotated = image::imageops::rotate90(rgba);
150 let src_raw = rotated.as_raw();
151 for row in 0..dh {
152 unsafe {
153 std::ptr::copy_nonoverlapping(
154 src_raw.as_ptr().add(row * dw * 4),
155 dst.add(((dy + row) * buf_stride + dx) * 4),
156 dw * 4,
157 );
158 }
159 }
160 } else {
161 let src_raw = rgba.as_raw();
162 let src_stride = rgba.width() as usize * 4;
163 for row in 0..dh {
164 unsafe {
165 std::ptr::copy_nonoverlapping(
166 src_raw.as_ptr().add(row * src_stride),
167 dst.add(((dy + row) * buf_stride + dx) * 4),
168 dw * 4,
169 );
170 }
171 }
172 }
173 });
174
175 let frames: Vec<FrameInfo> = pack_output
176 .placed
177 .iter()
178 .map(|ps| FrameInfo {
179 id: ps.placement.sprite_id.clone(),
180 x: ps.placement.dest.x,
181 y: ps.placement.dest.y,
182 w: ps.placement.dest.w,
183 h: ps.placement.dest.h,
184 alias_of: None,
185 })
186 .collect();
187
188 let atlas_frames: Vec<AtlasFrame> = pack_output
189 .placed
190 .iter()
191 .map(|ps| {
192 let trimmed = ps.sprite.trim_rect.is_some();
193 let sss = ps.sprite.trim_rect.unwrap_or(SourceRect {
194 x: 0,
195 y: 0,
196 w: ps.sprite.original_size.w,
197 h: ps.sprite.original_size.h,
198 });
199 AtlasFrame {
200 id: ps.placement.sprite_id.clone(),
201 frame: Rect::new(
202 ps.placement.dest.x,
203 ps.placement.dest.y,
204 ps.placement.dest.w,
205 ps.placement.dest.h,
206 ),
207 rotated: ps.placement.rotated,
208 trimmed,
209 sprite_source_size: sss,
210 source_size: ps.sprite.original_size,
211 polygon: ps.sprite.polygon.clone(),
212 nine_patch: ps.sprite.nine_patch,
213 pivot: ps.sprite.pivot,
214 alias_of: None,
215 }
216 })
217 .collect();
218
219 let width = pack_output.atlas_size.w;
220 let height = pack_output.atlas_size.h;
221 let rgba = canvas_raw;
222
223 Ok((
224 SheetOutput {
225 rgba,
226 width,
227 height,
228 frames,
229 atlas_frames,
230 },
231 overflow,
232 ))
233}
234
235pub fn run_pack(project: &Project) -> Result<WorkerOutput> {
239 let n = std::thread::available_parallelism()
240 .map(|p| p.get().saturating_sub(2).max(1))
241 .unwrap_or(1);
242 rayon::ThreadPoolBuilder::new()
243 .num_threads(n)
244 .build()
245 .map_err(|e| anyhow::anyhow!("{e}"))?
246 .install(|| run_pack_impl(project))
247}
248
249fn run_pack_impl(project: &Project) -> Result<WorkerOutput> {
250 let paths = collect_images(project);
252 if paths.is_empty() {
253 anyhow::bail!("no images found in the configured sources");
254 }
255
256 let mut sprites: Vec<Sprite> = paths
258 .par_iter()
259 .filter_map(|(path, id)| match loader::load(path, id.clone()) {
260 Ok(s) => Some(s),
261 Err(e) => {
262 tracing::warn!("failed to load {}: {e}", path.display());
263 None
264 }
265 })
266 .collect();
267 if sprites.is_empty() {
268 anyhow::bail!("all images failed to load");
269 }
270
271 let sprite_cfg = &project.config.sprites;
272
273 sprites
275 .par_iter_mut()
276 .for_each(|s| trim::trim(s, sprite_cfg));
277
278 if sprite_cfg.extrude > 0 {
280 sprites
281 .par_iter_mut()
282 .for_each(|s| extrude::extrude(s, sprite_cfg.extrude));
283 }
284
285 let sprite_count = sprites.len();
286
287 let (base_sprites, base_aliases) = if sprite_cfg.detect_aliases {
289 detect_aliases(sprites)
290 } else {
291 (sprites, Vec::new())
292 };
293 let alias_count = base_aliases.len();
294
295 let packer: Box<dyn Packer> = match &project.config.algorithm {
297 AlgorithmConfig::Grid {
298 cell_width,
299 cell_height,
300 } => Box::new(Grid {
301 cell_width: if *cell_width == 0 {
302 None
303 } else {
304 Some(*cell_width)
305 },
306 cell_height: if *cell_height == 0 {
307 None
308 } else {
309 Some(*cell_height)
310 },
311 }),
312 AlgorithmConfig::Basic => Box::new(Basic),
313 AlgorithmConfig::MaxRects { heuristic } => Box::new(MaxRects {
314 heuristic: *heuristic,
315 }),
316 AlgorithmConfig::Polygon => Box::new(MaxRects::default()),
317 };
318
319 let multipack = project.config.output.multipack;
321 let mut remaining = base_sprites;
322 let mut overflow_count = 0;
323 let mut sheets: Vec<SheetOutput> = Vec::new();
324
325 loop {
326 let (mut sheet, overflow) = build_sheet(packer.as_ref(), remaining, project)?;
327 remaining = overflow;
328
329 if sheets.is_empty() {
331 let alias_coords: Vec<(u32, u32, u32, u32)> = {
332 let frame_id_to_rect: std::collections::HashMap<&str, (u32, u32, u32, u32)> = sheet
333 .frames
334 .iter()
335 .map(|f| (f.id.as_str(), (f.x, f.y, f.w, f.h)))
336 .collect();
337 base_aliases
338 .iter()
339 .map(|alias| {
340 let canon = alias.alias_of.as_deref().unwrap_or("");
341 frame_id_to_rect.get(canon).copied().unwrap_or_default()
342 })
343 .collect()
344 };
345
346 for (alias, (x, y, w, h)) in base_aliases.iter().zip(alias_coords) {
347 sheet.frames.push(FrameInfo {
348 id: alias.id.clone(),
349 x,
350 y,
351 w,
352 h,
353 alias_of: alias.alias_of.clone(),
354 });
355 sheet.atlas_frames.push(AtlasFrame {
356 id: alias.id.clone(),
357 frame: Rect::new(x, y, w, h),
358 rotated: false,
359 trimmed: false,
360 sprite_source_size: SourceRect {
361 x: 0,
362 y: 0,
363 w: alias.original_size.w,
364 h: alias.original_size.h,
365 },
366 source_size: alias.original_size,
367 polygon: None,
368 nine_patch: alias.nine_patch,
369 pivot: alias.pivot,
370 alias_of: alias.alias_of.clone(),
371 });
372 }
373 }
374
375 sheets.push(sheet);
376
377 if remaining.is_empty() {
378 break;
379 }
380 if !multipack {
381 overflow_count = remaining.len();
382 break;
383 }
384 }
385
386 Ok(WorkerOutput {
387 sheets,
388 sprite_count,
389 alias_count,
390 overflow_count,
391 })
392}