1use yog_sync::LazyLock;
23
24use yog_graphics::{Rect, Vec2};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ImageSource {
29 Item(String),
31 Block(String),
33 Sprite(String),
36 ModPath(String),
39 Texture(String),
41 Video(String),
44}
45
46const LEGACY_TEX_SIZE: f32 = 256.0;
48
49pub fn texture_id(ns: &str, path: &str) -> String {
54 use yog_ids::res::{PNG, SVG, TEXTURES};
55 let path = path.trim_start_matches(yog_ids::sep::PATH);
56 if path.ends_with(SVG) {
57 let path = path.strip_prefix(TEXTURES).unwrap_or(path);
58 return yog_ids::join(ns, &format!("{TEXTURES}{path}"));
59 }
60 if path.starts_with(TEXTURES) || path.ends_with(PNG) {
61 let png = if path.ends_with(PNG) { "" } else { PNG };
62 yog_ids::join(ns, &format!("{path}{png}"))
63 } else {
64 yog_ids::join(ns, &format!("{TEXTURES}{path}{PNG}"))
65 }
66}
67
68pub fn short_source(texture: &str) -> String {
71 let (ns, path) = yog_ids::split(texture.trim());
72 let path = path.strip_prefix(yog_ids::res::TEXTURES).unwrap_or(path);
73 yog_ids::join(ns, path.strip_suffix(yog_ids::res::PNG).unwrap_or(path))
74}
75
76pub use yog_ids::image_scheme as scheme;
78
79impl std::fmt::Display for ImageSource {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 ImageSource::Item(id) => write!(f, "{}:{id}", scheme::ITEM),
85 ImageSource::Block(id) => write!(f, "{}:{id}", scheme::BLOCK),
86 ImageSource::Sprite(name) => write!(f, "{}:{name}", scheme::SPRITE),
87 ImageSource::ModPath(path) => write!(f, "{}:{path}", scheme::MOD),
88 ImageSource::Texture(id) => f.write_str(id),
89 ImageSource::Video(id) => write!(f, "{}:{id}", scheme::VIDEO),
90 }
91 }
92}
93
94impl ImageSource {
95 pub fn parse(source: &str) -> Option<ImageSource> {
97 let s = source.trim();
98 let (scheme, rest) = s.split_once(yog_ids::sep::NAMESPACE)?;
99 if rest.is_empty() {
100 return None;
101 }
102 Some(match scheme {
103 scheme::ITEM => ImageSource::Item(yog_ids::qualify(rest.trim())),
104 scheme::BLOCK => ImageSource::Block(yog_ids::qualify(rest.trim())),
105 scheme::SPRITE => ImageSource::Sprite(rest.to_string()),
106 scheme::MOD => ImageSource::ModPath(rest.to_string()),
107 scheme::VIDEO => ImageSource::Video(rest.trim().to_string()),
108 ns if !ns.is_empty() => ImageSource::Texture(texture_id(ns, rest)),
109 _ => return None,
110 })
111 }
112
113 pub fn with_mod_id(self, mod_id: &str) -> ImageSource {
115 match self {
116 ImageSource::ModPath(path) => ImageSource::Texture(texture_id(mod_id, &path)),
117 other => other,
118 }
119 }
120}
121
122pub fn suggest_alias(id: &str) -> Option<String> {
127 let (ns, path) = id.trim().split_once(yog_ids::sep::NAMESPACE)?;
128 let path = path.strip_prefix(yog_ids::res::TEXTURES)?;
129 let stem = path.strip_suffix(yog_ids::res::PNG).unwrap_or(path);
130 let short = |id: &str| -> String {
132 if ns == yog_ids::ns::MINECRAFT {
133 id.to_string()
134 } else {
135 yog_ids::join(ns, id)
136 }
137 };
138 if let Some(item) = stem.strip_prefix(yog_ids::res::ITEM_DIR) {
139 return Some(ImageSource::Item(short(item)).to_string());
140 }
141 if let Some(block) = stem.strip_prefix(yog_ids::res::BLOCK_DIR) {
142 return Some(ImageSource::Block(short(block)).to_string());
143 }
144 if ns == yog_ids::ns::MINECRAFT
145 && let Some(atlas_id) = stem.strip_prefix(yog_ids::res::GUI_SPRITES)
146 && let Some(sprite) = sprite_by_atlas(atlas_id)
147 {
148 return Some(ImageSource::Sprite(sprite.name.to_string()).to_string());
149 }
150 Some(yog_ids::join(ns, stem))
151}
152
153#[derive(Debug, Clone, PartialEq)]
155pub struct LegacyRegion {
156 pub texture: String,
158 pub region: Rect,
160 pub texture_size: Vec2,
162}
163
164#[derive(Debug, Clone, PartialEq)]
166pub struct Sprite {
167 pub name: String,
169 pub description: String,
171 pub size: Vec2,
173 pub atlas: Option<String>,
176 pub legacy: Option<LegacyRegion>,
178 pub own: Option<LegacyRegion>,
180 pub border: f32,
182}
183
184impl Sprite {
185 pub fn size(&self, atlas: bool) -> Vec2 {
187 match (atlas, &self.legacy) {
188 (false, Some(l)) => Vec2::new(l.region.w, l.region.h),
189 _ => self.size,
190 }
191 }
192
193 pub fn is_procedural(&self) -> bool {
195 self.atlas.is_none() && self.legacy.is_none() && self.own.is_none()
196 }
197
198 pub fn sources(&self) -> Vec<SpriteSource<'_>> {
202 let mut out = Vec::new();
203 if let Some(a) = &self.atlas {
204 out.push(SpriteSource::Atlas(yog_ids::qualify(a)));
205 }
206 out.extend(
207 self.legacy
208 .iter()
209 .chain(&self.own)
210 .map(SpriteSource::Region),
211 );
212 out
213 }
214}
215
216#[derive(Debug, Clone, PartialEq)]
218pub enum SpriteSource<'a> {
219 Atlas(String),
221 Region(&'a LegacyRegion),
223}
224
225const OWN: &[(&str, &[u8])] = &[
228 (
229 "textures/gui/panel.png",
230 include_bytes!("../res/assets/yog/textures/gui/panel.png"),
231 ),
232 (
233 "models/item/spawn_egg.json",
234 include_bytes!("../res/assets/yog/models/item/spawn_egg.json"),
235 ),
236 (
237 "textures/item/spawn_egg.png",
238 include_bytes!("../res/assets/yog/textures/item/spawn_egg.png"),
239 ),
240 (
241 "textures/item/spawn_egg_overlay.png",
242 include_bytes!("../res/assets/yog/textures/item/spawn_egg_overlay.png"),
243 ),
244];
245
246pub fn own_asset(path: &str) -> Option<&'static [u8]> {
248 OWN.iter().find(|(p, _)| *p == path).map(|(_, b)| *b)
249}
250
251pub fn sprite(name: &str) -> Option<&'static Sprite> {
253 SPRITES.iter().find(|s| s.name == name.trim())
254}
255
256pub fn sprite_by_atlas(atlas: &str) -> Option<&'static Sprite> {
258 SPRITES.iter().find(|s| s.atlas.as_deref() == Some(atlas))
259}
260
261pub fn atlas_texture(ns: &str, atlas: &str) -> String {
264 yog_ids::join(
265 ns,
266 &yog_ids::texture_file(&format!("{}{atlas}", yog_ids::res::GUI_SPRITES)),
267 )
268}
269
270#[derive(yog_data::Schema)]
272struct SpriteDef {
273 name: String,
274 description: String,
275 size: [f32; 2],
276 atlas: Option<String>,
277 legacy: Option<LegacyDef>,
278 own: Option<LegacyDef>,
279 #[data(default)]
280 border: f32,
281}
282
283#[derive(yog_data::Schema)]
284struct LegacyDef {
285 texture: String,
286 region: [f32; 4],
287 texture_size: Option<f32>,
289}
290
291fn region(l: LegacyDef) -> LegacyRegion {
292 let [u, v, w, h] = l.region;
293 let side = l.texture_size.unwrap_or(LEGACY_TEX_SIZE);
294 LegacyRegion {
295 texture: l.texture,
296 region: Rect::new(u, v, w, h),
297 texture_size: Vec2::new(side, side),
298 }
299}
300
301pub static SPRITES: LazyLock<Vec<Sprite>> = LazyLock::new(|| {
303 yog_data::built_in::<Vec<SpriteDef>>(
304 "yog-gfx's sprites",
305 include_str!("../assets/sprites.ydata"),
306 )
307 .into_iter()
308 .map(|d| Sprite {
309 name: d.name,
310 description: d.description,
311 size: Vec2::new(d.size[0], d.size[1]),
312 atlas: d.atlas,
313 legacy: d.legacy.map(region),
314 own: d.own.map(region),
315 border: d.border,
316 })
317 .collect()
318});
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
322#[repr(u8)]
323pub enum Stretch {
324 None = 0,
326 Fill = 1,
328 #[default]
330 Uniform = 2,
331 UniformToFill = 3,
333}
334
335impl Stretch {
336 pub const ALL: [(Stretch, &'static str); 4] = [
338 (Stretch::None, "None"),
339 (Stretch::Fill, "Fill"),
340 (Stretch::Uniform, "Uniform"),
341 (Stretch::UniformToFill, "UniformToFill"),
342 ];
343
344 pub fn name(self) -> &'static str {
346 Self::ALL
347 .iter()
348 .find(|(s, _)| *s == self)
349 .map_or("", |(_, n)| n)
350 }
351
352 pub fn parse(name: &str) -> Option<Stretch> {
354 let wanted = name.trim().replace('_', "");
355 Self::ALL
356 .iter()
357 .find(|(_, n)| n.eq_ignore_ascii_case(&wanted))
358 .map(|(s, _)| *s)
359 }
360}
361
362pub fn nine_slice(dest: Rect, region: Rect, border: f32) -> Vec<(Rect, Rect)> {
367 let Rect { x, y, w, h } = dest;
368 let Rect {
369 x: u,
370 y: v,
371 w: rw,
372 h: rh,
373 } = region;
374 let b = border.min(rw / 2.0).min(rh / 2.0).min(w / 2.0).min(h / 2.0);
375 if b <= 0.0 || (w == rw && h == rh) {
376 return vec![(dest, region)];
377 }
378 let cols = [
380 (x, b, u, b),
381 (x + b, w - 2.0 * b, u + b, rw - 2.0 * b),
382 (x + w - b, b, u + rw - b, b),
383 ];
384 let rows = [
385 (y, b, v, b),
386 (y + b, h - 2.0 * b, v + b, rh - 2.0 * b),
387 (y + h - b, b, v + rh - b, b),
388 ];
389 let mut out = Vec::with_capacity(cols.len() * rows.len());
390 for (dy, dh, sv, sh) in rows {
391 for (dx, dw, su, sw) in cols {
392 out.push((Rect::new(dx, dy, dw, dh), Rect::new(su, sv, sw, sh)));
393 }
394 }
395 out
396}
397
398pub fn fit(stretch: Stretch, natural: Vec2, bx: Rect) -> (Rect, Rect) {
403 let whole = Rect::new(0.0, 0.0, 1.0, 1.0);
404 if natural.x <= 0.0 || natural.y <= 0.0 || bx.w <= 0.0 || bx.h <= 0.0 {
405 return (Rect::new(bx.x, bx.y, 0.0, 0.0), whole);
406 }
407 let scale = match stretch {
408 Stretch::Fill => return (bx, whole),
409 Stretch::None => 1.0,
410 Stretch::Uniform => (bx.w / natural.x).min(bx.h / natural.y),
411 Stretch::UniformToFill => (bx.w / natural.x).max(bx.h / natural.y),
412 };
413 let d = natural * scale;
414 let axis = |pos: f32, size: f32, d: f32| -> (f32, f32, f32, f32) {
416 if d <= size {
417 ((pos + (size - d) / 2.0).floor(), d, 0.0, 1.0)
418 } else {
419 let cut = (d - size) / 2.0 / d;
420 (pos, size, cut, 1.0 - 2.0 * cut)
421 }
422 };
423 let (dx, dw, u, uw) = axis(bx.x, bx.w, d.x);
424 let (dy, dh, v, vh) = axis(bx.y, bx.h, d.y);
425 (Rect::new(dx, dy, dw, dh), Rect::new(u, v, uw, vh))
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn the_sprite_table_reads() {
434 assert!(
435 yog_data::from_text::<Vec<SpriteDef>>(include_str!("../assets/sprites.ydata")).is_ok()
436 );
437 }
438
439 #[test]
440 fn parses_sources() {
441 assert_eq!(
442 ImageSource::parse("item:apple"),
443 Some(ImageSource::Item("minecraft:apple".into()))
444 );
445 assert_eq!(
446 ImageSource::parse("item:mymod:wand"),
447 Some(ImageSource::Item("mymod:wand".into()))
448 );
449 assert_eq!(
450 ImageSource::parse("block:stone"),
451 Some(ImageSource::Block("minecraft:stone".into()))
452 );
453 assert_eq!(
454 ImageSource::parse("sprite:button"),
455 Some(ImageSource::Sprite("button".into()))
456 );
457 assert_eq!(
458 ImageSource::parse("mymod:gui/logo"),
459 Some(ImageSource::Texture("mymod:textures/gui/logo.png".into()))
460 );
461 assert_eq!(
462 ImageSource::parse("minecraft:textures/item/apple.png"),
463 Some(ImageSource::Texture(
464 "minecraft:textures/item/apple.png".into()
465 ))
466 );
467 assert_eq!(
468 ImageSource::parse("mod:gui/logo").map(|s| s.with_mod_id("mymod")),
469 Some(ImageSource::Texture("mymod:textures/gui/logo.png".into()))
470 );
471 assert_eq!(
472 ImageSource::parse("mymod:gui/logo.svg"),
473 Some(ImageSource::Texture("mymod:textures/gui/logo.svg".into()))
474 );
475 assert_eq!(
476 ImageSource::parse("mymod:textures/gui/logo.svg"),
477 Some(ImageSource::Texture("mymod:textures/gui/logo.svg".into()))
478 );
479 assert_eq!(ImageSource::parse(""), None);
480 assert_eq!(ImageSource::parse("apple"), None);
481 assert_eq!(ImageSource::parse("item:"), None);
482 }
483
484 #[test]
485 fn suggests_aliases_for_raw_paths() {
486 assert_eq!(
487 suggest_alias("minecraft:textures/item/apple.png").as_deref(),
488 Some("item:apple")
489 );
490 assert_eq!(
491 suggest_alias("mymod:textures/block/ore.png").as_deref(),
492 Some("block:mymod:ore")
493 );
494 assert_eq!(
495 suggest_alias("minecraft:textures/gui/widgets.png").as_deref(),
496 Some("minecraft:gui/widgets")
497 );
498 assert_eq!(
499 suggest_alias("minecraft:textures/gui/sprites/widget/button.png").as_deref(),
500 Some("sprite:button")
501 );
502 assert_eq!(
503 suggest_alias("mymod:textures/gui/logo.png").as_deref(),
504 Some("mymod:gui/logo")
505 );
506 assert_eq!(suggest_alias("item:apple"), None);
507 }
508
509 #[test]
510 fn sprite_names_are_unique() {
511 for (i, s) in SPRITES.iter().enumerate() {
512 assert!(
513 SPRITES[i + 1..].iter().all(|o| o.name != s.name),
514 "{}",
515 s.name
516 );
517 }
518 assert!(sprite("slot").unwrap().is_procedural());
519 assert_eq!(
520 sprite("hotbar_selection").unwrap().size(false),
521 Vec2::new(24.0, 24.0)
522 );
523 assert_eq!(
524 sprite("hotbar_selection").unwrap().size(true),
525 Vec2::new(24.0, 23.0)
526 );
527 }
528
529 #[test]
530 fn fits_like_wpf() {
531 let icon = Vec2::new(16.0, 16.0);
532 let bx = Rect::new(0.0, 0.0, 156.0, 29.0);
533 let whole = Rect::new(0.0, 0.0, 1.0, 1.0);
534 assert_eq!(
536 fit(Stretch::Uniform, icon, bx),
537 (Rect::new(63.0, 0.0, 29.0, 29.0), whole)
538 );
539 assert_eq!(
540 fit(Stretch::Fill, icon, Rect::new(1.0, 2.0, 156.0, 29.0)).0,
541 Rect::new(1.0, 2.0, 156.0, 29.0)
542 );
543 assert_eq!(
544 fit(Stretch::None, icon, bx).0,
545 Rect::new(70.0, 6.0, 16.0, 16.0)
546 );
547 let (d, s) = fit(Stretch::UniformToFill, icon, bx);
548 assert_eq!(d, bx);
549 assert!((s.y - (156.0 - 29.0) / 2.0 / 156.0).abs() < 1e-5, "{s:?}");
550 let (d, s) = fit(
552 Stretch::None,
553 Vec2::new(40.0, 10.0),
554 Rect::new(0.0, 0.0, 20.0, 10.0),
555 );
556 assert_eq!(d, Rect::new(0.0, 0.0, 20.0, 10.0));
557 assert_eq!(s, Rect::new(0.25, 0.0, 0.5, 1.0));
558 }
559}