Skip to main content

bevy_titan/
asset_loader.rs

1//! This module handles loading a TextureAtlas from a titan ron file.
2//!
3//! `bevy_titan` introduces a definition of a titan ron file and the corresponding [`SpriteSheetLoader`].
4//! Assets with the 'titan' extension can be loaded just like any other asset via the [`AssetServer`](::bevy::asset::AssetServer)
5//! and will yield a [`TextureAtlas`] [`Handle`](::bevy::asset::Handle).
6
7use 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/// Loader for spritesheet manifest files written in ron. Loads a TextureAtlas asset.
27#[derive(Default, TypePath)]
28pub struct SpriteSheetLoader;
29
30/// Possible errors that can be produced by [`SpriteSheetLoader`].
31#[non_exhaustive]
32#[derive(Debug, Error)]
33pub enum SpriteSheetLoaderError {
34    /// An [IOError](std::io::Error).
35    #[error("Could not load file: {0}")]
36    IoError(#[from] std::io::Error),
37    /// A [RonSpannedError](ron::error::SpannedError).
38    #[error("Could not parse RON: {0}")]
39    RonSpannedError(#[from] ron::error::SpannedError),
40    /// A [`LoadDirectError``].
41    #[error("Could not load: {0}")]
42    LoadDirectError(#[from] LoadDirectError),
43    /// A NotAnImageError.
44    #[error("Loading from {0} does not provide Image")]
45    NotAnImageError(String),
46    /// A [`TextureAtlasBuilderError`].
47    #[error("TextureAtlasBuilderError: {0}")]
48    TextureAtlasBuilderError(#[from] TextureAtlasBuilderError),
49    /// A NoEntriesError.
50    #[error("No entries were found")]
51    NoEntriesError,
52    /// An [`TextureExtractError`].
53    #[error("TextureExtractError: {0}")]
54    TextureExtractError(#[from] TextureExtractError),
55    /// A SizeMismatchError.
56    #[error("Configured initial size {0} is bigger than max size {1}")]
57    SizeMismatchError(UVec2, UVec2),
58}
59
60/// TextureExtractError.
61#[derive(Debug, Error)]
62pub enum TextureExtractError {
63    /// An InvalidRectError.
64    #[error("Rect with min {0} and max {1} is invalid for image {2}")]
65    InvalidRectError(UVec2, UVec2, String),
66    /// A [`TextureAccessError`].
67    #[error("TextureAccessError: {0}")]
68    TextureAccessError(#[from] TextureAccessError),
69}
70
71/// File extension for spritesheet manifest files written in ron.
72pub const FILE_EXTENSIONS: &[&str] = &["titan.ron", "titan"];
73
74/// TextureAtlas Asset
75#[derive(Debug, Asset, Reflect)]
76pub struct TextureAtlas {
77    /// Atlas Texture Image
78    pub texture: Handle<Image>,
79    /// Texture Atlas Layout
80    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            /* Load the image */
123            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            /* Get and insert all rects */
132            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            // TODO: Handle error?
233            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    /* TODO: Tests */
256}