1use std::path::Path;
8
9use bevy::{
10 asset::{
11 Asset, AssetLoader, AssetPath, Handle, LoadContext, LoadDirectError, RenderAssetUsages,
12 io::Reader,
13 },
14 image::{
15 Image, TextureAccessError, TextureAtlasBuilder, TextureAtlasBuilderError,
16 TextureAtlasLayout, TextureFormatPixelInfo,
17 },
18 math::{URect, UVec2},
19 reflect::{Reflect, TypePath},
20 render::render_resource::{Extent3d, TextureDimension},
21};
22use thiserror::Error;
23
24use crate::serde::{Titan, TitanEntry, TitanSpriteSheet};
25
26#[derive(Default, TypePath)]
28pub struct SpriteSheetLoader;
29
30#[non_exhaustive]
32#[derive(Debug, Error)]
33pub enum SpriteSheetLoaderError {
34 #[error("Could not load file: {0}")]
36 IoError(#[from] std::io::Error),
37 #[error("Could not parse RON: {0}")]
39 RonSpannedError(#[from] ron::error::SpannedError),
40 #[error("Could not load: {0}")]
42 LoadDirectError(#[from] LoadDirectError),
43 #[error("Loading from {0} does not provide Image")]
45 NotAnImageError(String),
46 #[error("TextureAtlasBuilderError: {0}")]
48 TextureAtlasBuilderError(#[from] TextureAtlasBuilderError),
49 #[error("No entries were found")]
51 NoEntriesError,
52 #[error("TextureExtractError: {0}")]
54 TextureExtractError(#[from] TextureExtractError),
55 #[error("Configured initial size {0} is bigger than max size {1}")]
57 SizeMismatchError(UVec2, UVec2),
58}
59
60#[derive(Debug, Error)]
62pub enum TextureExtractError {
63 #[error("Rect with min {0} and max {1} is invalid for image {2}")]
65 InvalidRectError(UVec2, UVec2, String),
66 #[error("TextureAccessError: {0}")]
68 TextureAccessError(#[from] TextureAccessError),
69}
70
71pub const FILE_EXTENSIONS: &[&str] = &["titan.ron", "titan"];
73
74#[derive(Debug, Asset, Reflect)]
76pub struct TextureAtlas {
77 pub texture: Handle<Image>,
79 pub layout: Handle<TextureAtlasLayout>,
81}
82
83impl AssetLoader for SpriteSheetLoader {
84 type Asset = TextureAtlas;
85 type Settings = ();
86 type Error = SpriteSheetLoaderError;
87
88 async fn load(
89 &self,
90 reader: &mut dyn Reader,
91 _settings: &Self::Settings,
92 load_context: &mut LoadContext<'_>,
93 ) -> Result<Self::Asset, Self::Error> {
94 let mut bytes = Vec::new();
95 reader.read_to_end(&mut bytes).await?;
96 let titan = ron::de::from_bytes::<Titan>(&bytes)?;
97
98 let configuration = titan.configuration;
99 if configuration.max_size.x < configuration.initial_size.x
100 || configuration.max_size.y < configuration.initial_size.y
101 {
102 return Err(SpriteSheetLoaderError::SizeMismatchError(
103 configuration.initial_size,
104 configuration.max_size,
105 ));
106 }
107
108 let titan_entries = titan.textures;
109 if titan_entries.is_empty() {
110 return Err(SpriteSheetLoaderError::NoEntriesError);
111 }
112
113 let images_len = titan_entries.iter().fold(0, |acc, titan_entry| {
114 acc + match &titan_entry.sprite_sheet {
115 TitanSpriteSheet::None => 1,
116 TitanSpriteSheet::Homogeneous { columns, rows, .. } => (columns * rows) as usize,
117 TitanSpriteSheet::Heterogeneous(vec) => vec.len(),
118 }
119 });
120 let mut images = Vec::with_capacity(images_len);
121 for titan_entry in titan_entries.into_iter() {
122 let titan_entry_path = titan_entry.path.clone();
124 let image_asset_path = AssetPath::from_path(Path::new(&titan_entry_path));
125 let image = load_context
126 .loader()
127 .immediate()
128 .load(image_asset_path)
129 .await?;
130
131 push_textures(&mut images, titan_entry, image.take())?;
133 }
134
135 let mut texture_atlas_builder = TextureAtlasBuilder::default();
136 texture_atlas_builder
137 .initial_size(configuration.initial_size)
138 .max_size(configuration.max_size)
139 .format(configuration.format)
140 .auto_format_conversion(configuration.auto_format_conversion)
141 .padding(configuration.padding);
142 for image in &images {
143 texture_atlas_builder.add_texture(None, image);
144 }
145 let (texture_atlas_layout, _, atlas_texture) = texture_atlas_builder.build()?;
146
147 let atlas_texture_handle =
148 load_context.add_loaded_labeled_asset("texture", atlas_texture.into());
149 let texture_atlas_layout_handle =
150 load_context.add_loaded_labeled_asset("layout", texture_atlas_layout.into());
151
152 let texture_atlas = TextureAtlas {
153 texture: atlas_texture_handle,
154 layout: texture_atlas_layout_handle,
155 };
156
157 Ok(texture_atlas)
158 }
159
160 fn extensions(&self) -> &[&str] {
161 FILE_EXTENSIONS
162 }
163}
164
165fn push_textures(
166 images: &mut Vec<Image>,
167 titan_entry: TitanEntry,
168 texture: Image,
169) -> Result<(), TextureExtractError> {
170 match titan_entry.sprite_sheet {
171 TitanSpriteSheet::None => {
172 images.push(texture);
173 }
174 TitanSpriteSheet::Homogeneous {
175 tile_size,
176 columns,
177 rows,
178 padding,
179 offset,
180 } => {
181 for i in 0..rows {
182 for j in 0..columns {
183 let min = UVec2::new(j, i) * tile_size
184 + offset
185 + (UVec2::new(1 + 2 * j, 1 + 2 * i) * padding);
186 let max = min + tile_size;
187 let rect = URect::from_corners(min, max);
188
189 let image = extract_texture_from_rect(&texture, rect)?;
190
191 images.push(image);
192 }
193 }
194 }
195 TitanSpriteSheet::Heterogeneous(rects) => {
196 for (position, size) in rects {
197 let min = position;
198 let max = min + size;
199 let rect = URect::from_corners(min, max);
200
201 let image = extract_texture_from_rect(&texture, rect)?;
202
203 images.push(image);
204 }
205 }
206 }
207
208 Ok(())
209}
210
211fn extract_texture_from_rect(image: &Image, rect: URect) -> Result<Image, TextureExtractError> {
212 if (rect.max.x > image.size().x) || (rect.max.y > image.size().y) {
213 Err(TextureExtractError::InvalidRectError(
214 rect.min,
215 rect.max,
216 String::from("Test"),
217 ))
218 } else {
219 let format_size = image.texture_descriptor.format.pixel_size()?;
220 let rect_size = UVec2::new(rect.max.x - rect.min.x, rect.max.y - rect.min.y);
221 let mut data: Vec<u8> = vec![0; (rect_size.x * rect_size.y) as usize * format_size];
222
223 for i in 0..rect_size.y {
224 let data_begin = (rect_size.x * i) as usize * format_size;
225 let data_end = data_begin + rect_size.x as usize * format_size;
226 let texture_atlas_rect_begin = (rect.min.x as usize
227 + (rect.min.y + i) as usize * image.width() as usize)
228 * format_size;
229 let texture_atlas_rect_end =
230 texture_atlas_rect_begin + rect_size.x as usize * format_size;
231
232 data[data_begin..data_end].copy_from_slice(
234 &image.data.as_ref().unwrap()[texture_atlas_rect_begin..texture_atlas_rect_end],
235 );
236 }
237
238 let image = Image::new(
239 Extent3d {
240 width: rect_size.x,
241 height: rect_size.y,
242 depth_or_array_layers: 1,
243 },
244 TextureDimension::D2,
245 data,
246 image.texture_descriptor.format,
247 RenderAssetUsages::MAIN_WORLD,
248 );
249 Ok(image)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 }