martin_core/resources/sprites/
mod.rs1use std::collections::HashMap;
20use std::fmt::Debug;
21use std::path::PathBuf;
22
23use dashmap::{DashMap, Entry};
24use futures::future::try_join_all;
25use log::{info, warn};
26use serde::{Deserialize, Serialize};
27pub use spreet::Spritesheet;
28use spreet::resvg::usvg::{Options, Tree};
29use spreet::{Sprite, SpritesheetBuilder, get_svg_input_paths, sprite_name};
30use tokio::io::AsyncReadExt;
31
32use self::SpriteError::{SpriteInstError, SpriteParsingError, SpriteProcessingError};
33
34mod error;
35pub use error::SpriteError;
36
37mod cache;
38pub use cache::{NO_SPRITE_CACHE, OptSpriteCache, SpriteCache};
39
40#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
42pub struct CatalogSpriteEntry {
43 pub images: Vec<String>,
45}
46
47pub type SpriteCatalog = HashMap<String, CatalogSpriteEntry>;
49
50#[derive(Debug, Clone, Default)]
52pub struct SpriteSources(DashMap<String, SpriteSource>);
53
54impl SpriteSources {
55 pub fn get_catalog(&self) -> Result<SpriteCatalog, SpriteError> {
57 let mut entries = SpriteCatalog::new();
59 for source in &self.0 {
60 let paths = get_svg_input_paths(&source.path, true)
61 .map_err(|e| SpriteProcessingError(e, source.path.clone()))?;
62 let mut images = Vec::with_capacity(paths.len());
63 for path in paths {
64 images.push(
65 sprite_name(&path, &source.path)
66 .map_err(|e| SpriteProcessingError(e, source.path.clone()))?,
67 );
68 }
69 images.sort();
70 entries.insert(source.key().clone(), CatalogSpriteEntry { images });
71 }
72 Ok(entries)
73 }
74
75 pub fn add_source(&mut self, id: String, path: PathBuf) {
78 let disp_path = path.display();
79 if path.is_file() {
80 warn!("Ignoring non-directory sprite source {id} from {disp_path}");
81 } else {
82 match self.0.entry(id) {
83 Entry::Occupied(v) => {
84 warn!(
85 "Ignoring duplicate sprite source {} from {disp_path} because it was already configured for {}",
86 v.key(),
87 v.get().path.display()
88 );
89 }
90 Entry::Vacant(v) => {
91 info!("Configured sprite source {} from {disp_path}", v.key());
92 v.insert(SpriteSource { path });
93 }
94 }
95 }
96 }
97
98 pub async fn get_sprites(&self, ids: &str, as_sdf: bool) -> Result<Spritesheet, SpriteError> {
103 let (ids, dpi) = if let Some(ids) = ids.strip_suffix("@2x") {
104 (ids, 2)
105 } else {
106 (ids, 1)
107 };
108
109 let sprite_ids = ids
110 .split(',')
111 .map(|id| self.get(id))
112 .collect::<Result<Vec<_>, SpriteError>>()?;
113
114 get_spritesheet(sprite_ids.iter(), dpi, as_sdf).await
115 }
116
117 fn get(&self, id: &str) -> Result<SpriteSource, SpriteError> {
118 match self.0.get(id) {
119 Some(v) => Ok(v.clone()),
120 None => Err(SpriteError::SpriteNotFound(id.to_string())),
121 }
122 }
123}
124
125#[derive(Clone, Debug)]
127pub struct SpriteSource {
128 path: PathBuf,
129}
130
131async fn parse_sprite(
133 name: String,
134 path: PathBuf,
135 pixel_ratio: u8,
136 as_sdf: bool,
137) -> Result<(String, Sprite), SpriteError> {
138 let on_err = |e| SpriteError::IoError(e, path.clone());
139
140 let mut file = tokio::fs::File::open(&path).await.map_err(on_err)?;
141
142 let mut buffer = Vec::new();
143 file.read_to_end(&mut buffer).await.map_err(on_err)?;
144
145 let tree = Tree::from_data(&buffer, &Options::default())
146 .map_err(|e| SpriteParsingError(e, path.clone()))?;
147
148 let sprite = if as_sdf {
149 Sprite::new_sdf(tree, pixel_ratio)
150 } else {
151 Sprite::new(tree, pixel_ratio)
152 };
153 let sprite = sprite.ok_or_else(|| SpriteInstError(path.clone()))?;
154
155 Ok((name, sprite))
156}
157
158pub async fn get_spritesheet(
160 sources: impl Iterator<Item = &SpriteSource>,
161 pixel_ratio: u8,
162 as_sdf: bool,
163) -> Result<Spritesheet, SpriteError> {
164 let mut futures = Vec::new();
166 for source in sources {
167 let paths = get_svg_input_paths(&source.path, true)
168 .map_err(|e| SpriteProcessingError(e, source.path.clone()))?;
169 for path in paths {
170 let name = sprite_name(&path, &source.path)
171 .map_err(|e| SpriteProcessingError(e, source.path.clone()))?;
172 futures.push(parse_sprite(name, path, pixel_ratio, as_sdf));
173 }
174 }
175 let sprites = try_join_all(futures).await?;
176 let mut builder = SpritesheetBuilder::new();
177 if as_sdf {
178 builder.make_sdf();
179 }
180 builder.sprites(sprites.into_iter().collect());
181
182 builder
186 .generate()
187 .ok_or(SpriteError::UnableToGenerateSpritesheet)
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[tokio::test]
195 async fn test_sprites() {
196 let mut sprites = SpriteSources::default();
197 sprites.add_source(
198 "src1".to_string(),
199 PathBuf::from("../tests/fixtures/sprites/src1"),
200 );
201 sprites.add_source(
202 "src2".to_string(),
203 PathBuf::from("../tests/fixtures/sprites/src2"),
204 );
205
206 assert_eq!(sprites.0.len(), 2);
207
208 for generate_sdf in [true, false] {
209 let paths = sprites
210 .0
211 .iter()
212 .map(|v| v.value().clone())
213 .collect::<Vec<_>>();
214 test_src(paths.iter(), 1, "all_1", generate_sdf).await;
215 test_src(paths.iter(), 2, "all_2", generate_sdf).await;
216
217 let src1_path = sprites.get("src1").into_iter().collect::<Vec<_>>();
218 test_src(src1_path.iter(), 1, "src1_1", generate_sdf).await;
219 test_src(src1_path.iter(), 2, "src1_2", generate_sdf).await;
220
221 let src2_path = sprites.get("src2").into_iter().collect::<Vec<_>>();
222 test_src(src2_path.iter(), 1, "src2_1", generate_sdf).await;
223 test_src(src2_path.iter(), 2, "src2_2", generate_sdf).await;
224 }
225 }
226
227 async fn test_src(
228 sources: impl Iterator<Item = &SpriteSource>,
229 pixel_ratio: u8,
230 filename: &str,
231 generate_sdf: bool,
232 ) {
233 let sprites = get_spritesheet(sources, pixel_ratio, generate_sdf)
234 .await
235 .unwrap();
236 let filename = if generate_sdf {
237 format!("{filename}_sdf")
238 } else {
239 filename.to_string()
240 };
241 insta::assert_json_snapshot!(format!("{filename}.json"), sprites.get_index());
242 let png = sprites.encode_png().unwrap();
243 insta::assert_binary_snapshot!(&format!("{filename}.png"), png);
244 }
245}