1use std::sync::LazyLock;
24
25use yog_graphics::{Rect, Vec2};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ImageSource {
30 Item(String),
32 Block(String),
34 Sprite(String),
37 ModPath(String),
40 Texture(String),
42}
43
44const LEGACY_TEX_SIZE: f32 = 256.0;
46
47pub fn texture_id(ns: &str, path: &str) -> String {
52 use yog_ids::res::{PNG, SVG, TEXTURES};
53 let path = path.trim_start_matches(yog_ids::sep::PATH);
54 if path.ends_with(SVG) {
55 let path = path.strip_prefix(TEXTURES).unwrap_or(path);
56 return yog_ids::join(ns, &format!("{TEXTURES}{path}"));
57 }
58 if path.starts_with(TEXTURES) || path.ends_with(PNG) {
59 let png = if path.ends_with(PNG) { "" } else { PNG };
60 yog_ids::join(ns, &format!("{path}{png}"))
61 } else {
62 yog_ids::join(ns, &format!("{TEXTURES}{path}{PNG}"))
63 }
64}
65
66pub fn short_source(texture: &str) -> String {
69 let (ns, path) = yog_ids::split(texture.trim());
70 let path = path.strip_prefix(yog_ids::res::TEXTURES).unwrap_or(path);
71 yog_ids::join(ns, path.strip_suffix(yog_ids::res::PNG).unwrap_or(path))
72}
73
74pub use yog_ids::image_scheme as scheme;
76
77impl std::fmt::Display for ImageSource {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 ImageSource::Item(id) => write!(f, "{}:{id}", scheme::ITEM),
83 ImageSource::Block(id) => write!(f, "{}:{id}", scheme::BLOCK),
84 ImageSource::Sprite(name) => write!(f, "{}:{name}", scheme::SPRITE),
85 ImageSource::ModPath(path) => write!(f, "{}:{path}", scheme::MOD),
86 ImageSource::Texture(id) => f.write_str(id),
87 }
88 }
89}
90
91impl ImageSource {
92 pub fn parse(source: &str) -> Option<ImageSource> {
94 let s = source.trim();
95 let (scheme, rest) = s.split_once(yog_ids::sep::NAMESPACE)?;
96 if rest.is_empty() {
97 return None;
98 }
99 Some(match scheme {
100 scheme::ITEM => ImageSource::Item(yog_ids::qualify(rest.trim())),
101 scheme::BLOCK => ImageSource::Block(yog_ids::qualify(rest.trim())),
102 scheme::SPRITE => ImageSource::Sprite(rest.to_string()),
103 scheme::MOD => ImageSource::ModPath(rest.to_string()),
104 ns if !ns.is_empty() => ImageSource::Texture(texture_id(ns, rest)),
105 _ => return None,
106 })
107 }
108
109 pub fn with_mod_id(self, mod_id: &str) -> ImageSource {
111 match self {
112 ImageSource::ModPath(path) => ImageSource::Texture(texture_id(mod_id, &path)),
113 other => other,
114 }
115 }
116}
117
118pub fn suggest_alias(id: &str) -> Option<String> {
123 let (ns, path) = id.trim().split_once(yog_ids::sep::NAMESPACE)?;
124 let path = path.strip_prefix(yog_ids::res::TEXTURES)?;
125 let stem = path.strip_suffix(yog_ids::res::PNG).unwrap_or(path);
126 let short = |id: &str| -> String {
128 if ns == yog_ids::ns::MINECRAFT {
129 id.to_string()
130 } else {
131 yog_ids::join(ns, id)
132 }
133 };
134 if let Some(item) = stem.strip_prefix(yog_ids::res::ITEM_DIR) {
135 return Some(ImageSource::Item(short(item)).to_string());
136 }
137 if let Some(block) = stem.strip_prefix(yog_ids::res::BLOCK_DIR) {
138 return Some(ImageSource::Block(short(block)).to_string());
139 }
140 if ns == yog_ids::ns::MINECRAFT
141 && let Some(atlas_id) = stem.strip_prefix(yog_ids::res::GUI_SPRITES)
142 && let Some(sprite) = sprite_by_atlas(atlas_id)
143 {
144 return Some(ImageSource::Sprite(sprite.name.to_string()).to_string());
145 }
146 Some(yog_ids::join(ns, stem))
147}
148
149#[derive(Debug, Clone, PartialEq)]
151pub struct LegacyRegion {
152 pub texture: String,
154 pub region: Rect,
156 pub texture_size: Vec2,
158}
159
160#[derive(Debug, Clone, PartialEq)]
162pub struct Sprite {
163 pub name: String,
165 pub description: String,
167 pub size: Vec2,
169 pub atlas: Option<String>,
172 pub legacy: Option<LegacyRegion>,
174 pub own: Option<LegacyRegion>,
176 pub border: f32,
178}
179
180impl Sprite {
181 pub fn size(&self, atlas: bool) -> Vec2 {
183 match (atlas, &self.legacy) {
184 (false, Some(l)) => Vec2::new(l.region.w, l.region.h),
185 _ => self.size,
186 }
187 }
188
189 pub fn is_procedural(&self) -> bool {
191 self.atlas.is_none() && self.legacy.is_none() && self.own.is_none()
192 }
193
194 pub fn sources(&self) -> Vec<SpriteSource<'_>> {
198 let mut out = Vec::new();
199 if let Some(a) = &self.atlas {
200 out.push(SpriteSource::Atlas(yog_ids::qualify(a)));
201 }
202 out.extend(self.legacy.iter().chain(&self.own).map(SpriteSource::Region));
203 out
204 }
205}
206
207#[derive(Debug, Clone, PartialEq)]
209pub enum SpriteSource<'a> {
210 Atlas(String),
212 Region(&'a LegacyRegion),
214}
215
216const OWN: &[(&str, &[u8])] = &[
219 ("textures/gui/panel.png", include_bytes!("../res/assets/yog/textures/gui/panel.png")),
220 ("models/item/spawn_egg.json", include_bytes!("../res/assets/yog/models/item/spawn_egg.json")),
221 ("textures/item/spawn_egg.png", include_bytes!("../res/assets/yog/textures/item/spawn_egg.png")),
222 ("textures/item/spawn_egg_overlay.png", include_bytes!("../res/assets/yog/textures/item/spawn_egg_overlay.png")),
223];
224
225pub fn own_asset(path: &str) -> Option<&'static [u8]> {
227 OWN.iter().find(|(p, _)| *p == path).map(|(_, b)| *b)
228}
229
230pub fn sprite(name: &str) -> Option<&'static Sprite> {
232 SPRITES.iter().find(|s| s.name == name.trim())
233}
234
235pub fn sprite_by_atlas(atlas: &str) -> Option<&'static Sprite> {
237 SPRITES.iter().find(|s| s.atlas.as_deref() == Some(atlas))
238}
239
240pub fn atlas_texture(ns: &str, atlas: &str) -> String {
243 yog_ids::join(ns, &yog_ids::texture_file(&format!("{}{atlas}", yog_ids::res::GUI_SPRITES)))
244}
245
246#[derive(yog_data::Schema)]
248struct SpriteDef {
249 name: String,
250 description: String,
251 size: [f32; 2],
252 atlas: Option<String>,
253 legacy: Option<LegacyDef>,
254 own: Option<LegacyDef>,
255 #[data(default)]
256 border: f32,
257}
258
259#[derive(yog_data::Schema)]
260struct LegacyDef {
261 texture: String,
262 region: [f32; 4],
263 texture_size: Option<f32>,
265}
266
267fn region(l: LegacyDef) -> LegacyRegion {
268 let [u, v, w, h] = l.region;
269 let side = l.texture_size.unwrap_or(LEGACY_TEX_SIZE);
270 LegacyRegion { texture: l.texture, region: Rect::new(u, v, w, h), texture_size: Vec2::new(side, side) }
271}
272
273pub static SPRITES: LazyLock<Vec<Sprite>> = LazyLock::new(|| {
275 yog_data::from_text::<Vec<SpriteDef>>(include_str!("../assets/sprites.ydata"))
276 .unwrap_or_else(|e| panic!("yog-gfx's sprites: {e}"))
277 .value
278 .into_iter()
279 .map(|d| Sprite {
280 name: d.name,
281 description: d.description,
282 size: Vec2::new(d.size[0], d.size[1]),
283 atlas: d.atlas,
284 legacy: d.legacy.map(region),
285 own: d.own.map(region),
286 border: d.border,
287 })
288 .collect()
289});
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
293#[repr(u8)]
294pub enum Stretch {
295 None = 0,
297 Fill = 1,
299 #[default]
301 Uniform = 2,
302 UniformToFill = 3,
304}
305
306pub fn fit(stretch: Stretch, natural: Vec2, bx: Rect) -> (Rect, Rect) {
311 let whole = Rect::new(0.0, 0.0, 1.0, 1.0);
312 if natural.x <= 0.0 || natural.y <= 0.0 || bx.w <= 0.0 || bx.h <= 0.0 {
313 return (Rect::new(bx.x, bx.y, 0.0, 0.0), whole);
314 }
315 let scale = match stretch {
316 Stretch::Fill => return (bx, whole),
317 Stretch::None => 1.0,
318 Stretch::Uniform => (bx.w / natural.x).min(bx.h / natural.y),
319 Stretch::UniformToFill => (bx.w / natural.x).max(bx.h / natural.y),
320 };
321 let d = natural * scale;
322 let axis = |pos: f32, size: f32, d: f32| -> (f32, f32, f32, f32) {
324 if d <= size {
325 ((pos + (size - d) / 2.0).floor(), d, 0.0, 1.0)
326 } else {
327 let cut = (d - size) / 2.0 / d;
328 (pos, size, cut, 1.0 - 2.0 * cut)
329 }
330 };
331 let (dx, dw, u, uw) = axis(bx.x, bx.w, d.x);
332 let (dy, dh, v, vh) = axis(bx.y, bx.h, d.y);
333 (Rect::new(dx, dy, dw, dh), Rect::new(u, v, uw, vh))
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn parses_sources() {
342 assert_eq!(
343 ImageSource::parse("item:apple"),
344 Some(ImageSource::Item("minecraft:apple".into()))
345 );
346 assert_eq!(
347 ImageSource::parse("item:mymod:wand"),
348 Some(ImageSource::Item("mymod:wand".into()))
349 );
350 assert_eq!(
351 ImageSource::parse("block:stone"),
352 Some(ImageSource::Block("minecraft:stone".into()))
353 );
354 assert_eq!(
355 ImageSource::parse("sprite:button"),
356 Some(ImageSource::Sprite("button".into()))
357 );
358 assert_eq!(
359 ImageSource::parse("mymod:gui/logo"),
360 Some(ImageSource::Texture("mymod:textures/gui/logo.png".into()))
361 );
362 assert_eq!(
363 ImageSource::parse("minecraft:textures/item/apple.png"),
364 Some(ImageSource::Texture(
365 "minecraft:textures/item/apple.png".into()
366 ))
367 );
368 assert_eq!(
369 ImageSource::parse("mod:gui/logo").map(|s| s.with_mod_id("mymod")),
370 Some(ImageSource::Texture("mymod:textures/gui/logo.png".into()))
371 );
372 assert_eq!(
373 ImageSource::parse("mymod:gui/logo.svg"),
374 Some(ImageSource::Texture("mymod:textures/gui/logo.svg".into()))
375 );
376 assert_eq!(
377 ImageSource::parse("mymod:textures/gui/logo.svg"),
378 Some(ImageSource::Texture("mymod:textures/gui/logo.svg".into()))
379 );
380 assert_eq!(ImageSource::parse(""), None);
381 assert_eq!(ImageSource::parse("apple"), None);
382 assert_eq!(ImageSource::parse("item:"), None);
383 }
384
385 #[test]
386 fn suggests_aliases_for_raw_paths() {
387 assert_eq!(
388 suggest_alias("minecraft:textures/item/apple.png").as_deref(),
389 Some("item:apple")
390 );
391 assert_eq!(
392 suggest_alias("mymod:textures/block/ore.png").as_deref(),
393 Some("block:mymod:ore")
394 );
395 assert_eq!(
396 suggest_alias("minecraft:textures/gui/widgets.png").as_deref(),
397 Some("minecraft:gui/widgets")
398 );
399 assert_eq!(
400 suggest_alias("minecraft:textures/gui/sprites/widget/button.png").as_deref(),
401 Some("sprite:button")
402 );
403 assert_eq!(
404 suggest_alias("mymod:textures/gui/logo.png").as_deref(),
405 Some("mymod:gui/logo")
406 );
407 assert_eq!(suggest_alias("item:apple"), None);
408 }
409
410 #[test]
411 fn sprite_names_are_unique() {
412 for (i, s) in SPRITES.iter().enumerate() {
413 assert!(
414 SPRITES[i + 1..].iter().all(|o| o.name != s.name),
415 "{}",
416 s.name
417 );
418 }
419 assert!(sprite("slot").unwrap().is_procedural());
420 assert_eq!(
421 sprite("hotbar_selection").unwrap().size(false),
422 Vec2::new(24.0, 24.0)
423 );
424 assert_eq!(sprite("hotbar_selection").unwrap().size(true), Vec2::new(24.0, 23.0));
425 }
426
427 #[test]
428 fn fits_like_wpf() {
429 let icon = Vec2::new(16.0, 16.0);
430 let bx = Rect::new(0.0, 0.0, 156.0, 29.0);
431 let whole = Rect::new(0.0, 0.0, 1.0, 1.0);
432 assert_eq!(fit(Stretch::Uniform, icon, bx), (Rect::new(63.0, 0.0, 29.0, 29.0), whole));
434 assert_eq!(fit(Stretch::Fill, icon, Rect::new(1.0, 2.0, 156.0, 29.0)).0, Rect::new(1.0, 2.0, 156.0, 29.0));
435 assert_eq!(fit(Stretch::None, icon, bx).0, Rect::new(70.0, 6.0, 16.0, 16.0));
436 let (d, s) = fit(Stretch::UniformToFill, icon, bx);
437 assert_eq!(d, bx);
438 assert!((s.y - (156.0 - 29.0) / 2.0 / 156.0).abs() < 1e-5, "{s:?}");
439 let (d, s) = fit(Stretch::None, Vec2::new(40.0, 10.0), Rect::new(0.0, 0.0, 20.0, 10.0));
441 assert_eq!(d, Rect::new(0.0, 0.0, 20.0, 10.0));
442 assert_eq!(s, Rect::new(0.25, 0.0, 0.5, 1.0));
443 }
444}