1use crate::content::refs::RefKind;
11use std::cell::RefCell;
12use std::path::{Path, PathBuf};
13use yog_sync::Arc;
14
15use yog_gfx::image::{
16 ImageSource, LegacyRegion, SPRITES, Sprite, SpriteSource, Stretch, atlas_texture, fit,
17 sprite as find_sprite,
18};
19use yog_gfx::{Color, Rect, Vec2};
20use yog_ui::widget::{self, Widget};
21
22use super::assets::{Image, Resources};
23use super::markup::{YuiNode, color};
24use super::raster::Canvas;
25
26struct Doc {
27 res: Resources,
28 mod_id: Option<String>,
29}
30
31thread_local! {
32 static DOC: RefCell<Option<Doc>> = const { RefCell::new(None) };
33}
34
35pub fn set_document(mod_root: Option<&Path>) {
39 let doc = Doc {
40 res: Resources::for_mod(mod_root),
41 mod_id: mod_root.and_then(crate::design::project::mod_id),
42 };
43 DOC.with(|d| *d.borrow_mut() = Some(doc));
44}
45
46pub fn resources() -> Resources {
48 DOC.with(|d| d.borrow().as_ref().map(|d| d.res.clone()))
49 .unwrap_or_else(|| Resources::for_mod(None))
50}
51
52fn with_doc<R>(f: impl FnOnce(Option<&Doc>) -> R) -> R {
53 DOC.with(|d| f(d.borrow().as_ref()))
54}
55
56pub fn resolve_mod(source: &str) -> String {
59 let source = source.trim();
60 match source.strip_prefix("mod:") {
61 Some(path) => match with_doc(|d| d.and_then(|d| d.mod_id.clone())) {
62 Some(id) => format!("{id}:{path}"),
63 None => source.to_string(),
64 },
65 None => source.to_string(),
66 }
67}
68
69pub fn stretch(v: &str) -> Stretch {
71 Stretch::parse(v).unwrap_or_default()
72}
73
74fn uv(v: &str) -> Option<[f32; 4]> {
76 let p: Vec<f32> = v
77 .split([' ', ','])
78 .filter(|s| !s.is_empty())
79 .map(|s| s.parse().ok())
80 .collect::<Option<_>>()?;
81 <[f32; 4]>::try_from(p)
82 .ok()
83 .filter(|r| r[2] > 0.0 && r[3] > 0.0)
84}
85
86fn rect([x, y, w, h]: [f32; 4]) -> Rect {
87 Rect::new(x, y, w, h)
88}
89
90pub fn widget(node: &YuiNode) -> Widget {
93 let a = |k: &str| node.attr(k);
94 let source = a(yog_ids::yui_attr::SOURCE).unwrap_or_default();
95 let mut w = widget::image(resolve_mod(source));
96 if let Some(s) = a(yog_ids::yui_attr::STRETCH) {
97 w = w.stretch(stretch(s));
98 }
99 if let Some(r) = a(yog_ids::yui_attr::UV).and_then(uv) {
100 w = w.region(rect(r));
101 }
102 if let Some(t) = a(yog_ids::yui_attr::TINT).and_then(color) {
103 w = w.tint(Color(t));
104 }
105 w
106}
107
108fn texture(id: &str) -> Option<Arc<Image>> {
109 if let Some(sheet) = book_sheet(id) {
110 return Some(sheet);
111 }
112 with_doc(|d| d.and_then(|d| d.res.texture(id)))
113}
114
115fn book_sheet(id: &str) -> Option<Arc<Image>> {
118 static SHEETS: yog_sync::OnceLock<[Option<Arc<Image>>; 2]> = yog_sync::OnceLock::new();
119 let [book, crafting] = SHEETS.get_or_init(|| {
120 [
121 super::assets::decode_png(yog_book::BOOK_PNG).map(Arc::new),
122 super::assets::decode_png(yog_book::CRAFTING_PNG).map(Arc::new),
123 ]
124 });
125 let texture = |sheet: &str| match ImageSource::parse(sheet) {
126 Some(ImageSource::Texture(t)) => t,
127 _ => String::new(),
128 };
129 match id {
130 _ if id == texture(yog_book::BOOK_SHEET) => book.clone(),
131 _ if id == texture(yog_book::CRAFTING_SHEET) => crafting.clone(),
132 _ => None,
133 }
134}
135
136fn atlas_image(id: &str) -> Option<Arc<Image>> {
138 let (ns, path) = yog_ids::split(id);
139 texture(&atlas_texture(ns, path))
140}
141
142fn resolve(sp: &Sprite) -> Option<SpriteSource<'_>> {
144 sp.sources().into_iter().find(|s| match s {
145 SpriteSource::Atlas(id) => atlas_image(id).is_some(),
146 SpriteSource::Region(r) => texture(&r.texture).is_some(),
147 })
148}
149
150fn in_game_files(kind: RefKind, id: String) -> String {
153 let version = with_doc(|d| d.and_then(|d| d.res.vanilla.as_ref().map(|v| v.version.clone())));
154 version
155 .and_then(|v| crate::content::refs::equivalent(kind, &id, &v))
156 .unwrap_or(id)
157}
158
159fn item_texture(id: &str, block: bool) -> Option<Arc<Image>> {
162 if !block
164 && let Some(super::item_models::Look::Flat(img)) =
165 with_doc(|d| d.and_then(|d| super::item_models::look(&d.res, id)))
166 {
167 return Some(img);
168 }
169 let (ns, name) = yog_ids::split(id);
170 let item = format!("{ns}:textures/item/{name}.png");
171 let blocks =
172 ["", "_front", "_side", "_top"].map(|s| format!("{ns}:textures/block/{name}{s}.png"));
173 let mut order: Vec<&String> = blocks.iter().collect();
174 if block {
175 order.push(&item);
176 } else {
177 order.insert(0, &item);
178 }
179 order
180 .into_iter()
181 .find_map(|t| texture(t))
182 .or_else(|| content_icon(id))
183}
184
185fn content_icon(id: &str) -> Option<Arc<Image>> {
187 type Cache = yog_sync::Mutex<std::collections::HashMap<(PathBuf, String), Option<Arc<Image>>>>;
188 static CACHE: yog_sync::OnceLock<Cache> = yog_sync::OnceLock::new();
189 let root = with_doc(|d| d.and_then(|d| d.res.mod_root.clone()))?;
190 let key = (root.clone(), id.to_string());
191 let cache = CACHE.get_or_init(Default::default);
192 if let Some(hit) = cache.lock().get(&key) {
193 return hit.clone();
194 }
195 let img = crate::design::content::content_icon(&root, id)
196 .and_then(|b| super::assets::decode_png(&b))
197 .map(Arc::new);
198 cache.lock().insert(key, img.clone());
199 img
200}
201
202fn item_3d(c: &mut Canvas, id: &str, [x, y, w, h]: [f32; 4]) -> bool {
208 match super::item_models::look(c.resources(), id) {
209 Some(super::item_models::Look::Flat(_)) => false,
210 Some(super::item_models::Look::Head(faces)) => {
211 let m = w.min(h) * HEAD_MARGIN;
212 super::blocks3d::draw_cube(
213 c,
214 faces,
215 [x + m, y + m, w - 2.0 * m, h - 2.0 * m],
216 SLOT_YAW,
217 SLOT_PITCH,
218 );
219 true
220 }
221 None if super::blocks3d::is_block(c.resources(), id) => {
222 super::blocks3d::draw(
223 c,
224 &format!("0 0 0 {id}"),
225 [x, y, w, h],
226 SLOT_YAW,
227 SLOT_PITCH,
228 );
229 true
230 }
231 None => false,
232 }
233}
234
235use yog_book::renderer::{BLOCK_PITCH as SLOT_PITCH, SLOT_YAW};
237const HEAD_MARGIN: f32 = 0.1;
239
240pub fn source_size(source: &str) -> Option<(f32, f32)> {
243 match ImageSource::parse(source)? {
244 ImageSource::Item(_) | ImageSource::Block(_) => {
245 Some((yog_ui::ICON_SIZE, yog_ui::ICON_SIZE))
246 }
247 ImageSource::Sprite(name) => find_sprite(&name).map(|s| match resolve(s) {
248 Some(SpriteSource::Region(r)) => (r.region.w, r.region.h),
249 _ => (s.size.x, s.size.y),
250 }),
251 ImageSource::Texture(id) if id.ends_with(".svg") => {
252 svg_document(&id).and_then(|d| yog_media::svg::size(&d))
253 }
254 ImageSource::Texture(id) => texture(&id).map(|i| (i.width as f32, i.height as f32)),
255 ImageSource::ModPath(_) => None,
256 }
257}
258
259fn tint(c: u32) -> [f32; 4] {
261 Color(c).to_f32()
262}
263
264fn blit_px(c: &mut Canvas, img: &Image, dest: [f32; 4], src: [f32; 4], t: u32) {
266 let [x, y, w, h] = dest;
267 let [u, v, uw, vh] = src;
268 if w <= 0.0 || h <= 0.0 || uw <= 0.0 || vh <= 0.0 {
269 return;
270 }
271 let (iw, ih) = (img.width as f32, img.height as f32);
272 c.blit(
273 img,
274 [x, y, w, h],
275 [u / iw, v / ih, (u + uw) / iw, (v + vh) / ih],
276 tint(t),
277 );
278}
279
280fn slot(c: &mut Canvas, [x, y, w, h]: [f32; 4]) {
282 for (r, color) in yog_ui::inv_slot_bevel(yog_gfx::Rect::new(x, y, w, h)) {
283 c.fill([r.x, r.y, r.x + r.w, r.y + r.h], color.0, color.0);
284 }
285}
286
287fn sprite(c: &mut Canvas, sp: &Sprite, dest: [f32; 4], part: [f32; 4], t: u32) {
290 let whole = part[2] <= 0.0 || part[3] <= 0.0;
291 let mut put = |img: &Image, src: [f32; 4], natural: f32, border: f32| {
293 if whole {
294 for (to, from) in yog_gfx::image::nine_slice(dest, src, border) {
295 blit_px(c, img, to, from, t);
296 }
297 } else {
298 let k = src[2] / natural;
299 blit_px(
300 c,
301 img,
302 dest,
303 [
304 src[0] + part[0] * k,
305 src[1] + part[1] * k,
306 part[2] * k,
307 part[3] * k,
308 ],
309 t,
310 );
311 }
312 };
313 if sp.is_procedural() {
314 slot(c, dest);
315 return;
316 }
317 let source = resolve(sp);
318 if let Some(SpriteSource::Atlas(id)) = &source
319 && let Some(img) = atlas_image(id)
320 {
321 let size = sp.size;
323 let frame_h = if img.height > img.width && size.y <= size.x {
324 img.width as f32 * size.y / size.x
325 } else {
326 img.height as f32
327 };
328 put(
329 &img,
330 [0.0, 0.0, img.width as f32, frame_h],
331 size.x,
332 sp.border,
333 );
334 return;
335 }
336 if let Some(SpriteSource::Region(LegacyRegion {
337 texture: tex,
338 region,
339 texture_size,
340 })) = source
341 && let Some(img) = texture(tex)
342 {
343 let k = img.width as f32 / texture_size.x;
345 put(
346 &img,
347 [region.x * k, region.y * k, region.w * k, region.h * k],
348 region.w,
349 sp.border * k,
350 );
351 return;
352 }
353 missing(c, dest);
354}
355
356fn missing(c: &mut Canvas, dest: [f32; 4]) {
357 blit_px(
358 c,
359 &super::assets::missing_image(),
360 dest,
361 [0.0, 0.0, yog_ui::ICON_SIZE, yog_ui::ICON_SIZE],
362 Color::WHITE.0,
363 );
364}
365
366const SVG_OVERSAMPLE: f32 = 4.0;
368
369fn svg_image(data: &str, w: u32, h: u32) -> Option<Image> {
371 Some(Image {
372 width: w,
373 height: h,
374 rgba: yog_media::svg::rasterize(data, w, h)?,
375 })
376}
377
378fn svg_document(id: &str) -> Option<String> {
380 let (ns, path) = yog_ids::split(id);
381 let bytes = with_doc(|d| d.and_then(|d| d.res.read(ns, path)))?;
382 String::from_utf8(bytes).ok()
383}
384
385pub fn draw_svg(c: &mut Canvas, data: &str, dest: [f32; 4], t: u32) {
387 if dest[2] <= 0.0 || dest[3] <= 0.0 {
388 return;
389 }
390 let side = |v: f32| ((v * SVG_OVERSAMPLE).ceil() as u32).clamp(1, 2048);
391 match svg_image(data, side(dest[2]), side(dest[3])) {
392 Some(img) => blit_px(
393 c,
394 &img,
395 dest,
396 [0.0, 0.0, img.width as f32, img.height as f32],
397 t,
398 ),
399 None => missing(c, dest),
400 }
401}
402
403const ENTITY_EGG_MAX: f32 = 2.0 * yog_ui::ICON_SIZE;
405
406pub fn draw_entity(c: &mut Canvas, entity: &str, [x, y, w, h]: [f32; 4]) {
409 let (ns, path) = yog_ids::split(entity);
410 let side = w.min(h).min(ENTITY_EGG_MAX);
411 let dest = [x + (w - side) / 2.0, y + (h - side) / 2.0, side, side];
412 let egg = yog_ids::join(ns, &format!("{path}{}", yog_ids::res::SPAWN_EGG_SUFFIX));
413 draw(
414 c,
415 &yog_ids::join(yog_ids::image_scheme::ITEM, &egg),
416 dest,
417 [0.0; 4],
418 Color::WHITE.0,
419 );
420}
421
422pub fn draw(c: &mut Canvas, source: &str, dest: [f32; 4], region: [f32; 4], t: u32) {
425 if dest[2] <= 0.0 || dest[3] <= 0.0 {
426 return;
427 }
428 if let Some(ImageSource::Texture(id)) = ImageSource::parse(source)
429 && id.ends_with(".svg")
430 {
431 match svg_document(&id) {
432 Some(doc) => draw_svg(c, &doc, dest, t),
433 None => missing(c, dest),
434 }
435 return;
436 }
437 let first_frame = |img: &Image| [0.0, 0.0, img.width as f32, img.width.min(img.height) as f32];
438 let source = match ImageSource::parse(source) {
440 Some(ImageSource::Item(id)) => Some(ImageSource::Item(in_game_files(RefKind::Item, id))),
441 Some(ImageSource::Block(id)) => Some(ImageSource::Block(in_game_files(RefKind::Block, id))),
442 other => other,
443 };
444 match source {
445 Some(ImageSource::Item(id)) if item_3d(c, &id, dest) => {}
446 Some(ImageSource::Item(id)) => match item_texture(&id, false) {
447 Some(img) => blit_px(c, &img, dest, first_frame(&img), Color::WHITE.0),
448 None => missing(c, dest),
449 },
450 Some(ImageSource::Block(id)) => match item_texture(&id, true) {
451 Some(img) => blit_px(c, &img, dest, first_frame(&img), Color::WHITE.0),
452 None => missing(c, dest),
453 },
454 Some(ImageSource::Sprite(name)) => match find_sprite(&name) {
455 Some(sp) => sprite(c, sp, dest, region, t),
456 None => match name
457 .split_once(':')
458 .and_then(|(ns, p)| texture(&atlas_texture(ns, p)))
459 {
460 Some(img) => blit_px(
461 c,
462 &img,
463 dest,
464 [0.0, 0.0, img.width as f32, img.height as f32],
465 t,
466 ),
467 None => missing(c, dest),
468 },
469 },
470 Some(ImageSource::Texture(id)) => match texture(&id) {
471 Some(img) => {
472 let src = if region[2] > 0.0 && region[3] > 0.0 {
473 region
474 } else {
475 [0.0, 0.0, img.width as f32, img.height as f32]
476 };
477 blit_px(c, &img, dest, src, t);
478 }
479 None => missing(c, dest),
480 },
481 Some(ImageSource::ModPath(_)) | None => missing(c, dest),
482 }
483}
484
485#[derive(Debug, Clone, yog_data::Schema)]
487pub struct Candidate {
488 pub source: String,
490 pub group: String,
492 pub detail: String,
493}
494
495pub fn candidates(path: &Path) -> Vec<Candidate> {
498 let root = super::mod_root(path);
499 let mut out = Vec::new();
500 if let Some(root) = &root {
501 out.extend(crate::media::pictures(root).into_iter().map(|p| Candidate {
502 source: ImageSource::ModPath(p.path()).to_string(),
503 group: "mod".into(),
504 detail: format!("@{}.{}", p.set, p.name),
505 }));
506 }
507 out.extend(SPRITES.iter().map(|s| Candidate {
508 source: format!("sprite:{}", s.name),
509 group: "sprite".into(),
510 detail: format!("{} ({}×{})", s.description, s.size.x, s.size.y),
511 }));
512 let res = Resources::for_mod(root.as_deref());
513 if let Some(v) = &res.vanilla {
514 let (items, blocks) = v.texture_names();
515 out.extend(items.into_iter().map(|n| Candidate {
516 source: yog_ids::join(yog_ids::image_scheme::ITEM, &n),
517 group: yog_ids::image_scheme::ITEM.into(),
518 detail: String::new(),
519 }));
520 out.extend(blocks.into_iter().map(|n| Candidate {
521 source: yog_ids::join(yog_ids::image_scheme::BLOCK, &n),
522 group: yog_ids::image_scheme::BLOCK.into(),
523 detail: String::new(),
524 }));
525 }
526 out
527}
528
529#[derive(yog_data::Schema)]
534pub struct Preview {
535 pub url: Option<String>,
536 pub alias: Option<String>,
537}
538
539pub fn preview(path: &Path, source: &str, size: u32) -> Preview {
540 let alias = yog_gfx::image::suggest_alias(source);
541 let url = draw_preview(path, alias.as_deref().unwrap_or(source), size);
542 Preview { url, alias }
543}
544
545const MIN_PREVIEW: u32 = 8;
547const MAX_PREVIEW: u32 = 256;
548const ICON_SIDE: f32 = 16.0;
550
551fn draw_preview(path: &Path, source: &str, size: u32) -> Option<String> {
552 let root = super::mod_root(path);
553 set_document(root.as_deref());
554 let res = Resources::for_mod(root.as_deref());
555 let source = resolve_mod(source);
556 let (nw, nh) = source_size(&source).unwrap_or((ICON_SIDE, ICON_SIDE));
557 let size = size.clamp(MIN_PREVIEW, MAX_PREVIEW);
558 let (dest, _) = fit(
559 Stretch::Uniform,
560 Vec2::new(nw, nh),
561 Rect::new(0.0, 0.0, size as f32, size as f32),
562 );
563 let mut c = Canvas::new(size, size, 1, &res);
564 let [x, y, w, h] = [dest.x, dest.y, dest.w, dest.h];
565 draw(&mut c, &source, [x, y, w, h], [0.0; 4], Color::WHITE.0);
566 c.png_url()
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
576 #[ignore]
577 fn sample_png() {
578 use base64::Engine as _;
579 let Ok(out) = std::env::var(yog_ids::env::IMAGES_PNG) else {
580 return;
581 };
582 let n = |uid: &str, attrs: &[(&str, &str)]| YuiNode {
583 uid: uid.into(),
584 kind: "Image".into(),
585 attrs: attrs
586 .iter()
587 .map(|(k, v)| (k.to_string(), v.to_string()))
588 .collect(),
589 children: vec![],
590 };
591 let mut root = n(
592 "s",
593 &[("width", "200"), ("height", "120"), ("bg", "#C0101010")],
594 );
595 root.kind = "Screen".into();
596 root.children = vec![
597 n(
599 "a",
600 &[
601 ("x", "4"),
602 ("y", "4"),
603 ("w", "156"),
604 ("h", "29"),
605 ("source", "minecraft:item/apple"),
606 ("bg", "#40FFFFFF"),
607 ],
608 ),
609 n(
610 "b",
611 &[
612 ("x", "4"),
613 ("y", "36"),
614 ("w", "32"),
615 ("h", "32"),
616 ("source", "item:apple"),
617 ],
618 ),
619 n(
620 "c",
621 &[
622 ("x", "40"),
623 ("y", "36"),
624 ("w", "32"),
625 ("h", "32"),
626 ("source", "block:crafting_table"),
627 ],
628 ),
629 n(
630 "d",
631 &[
632 ("x", "76"),
633 ("y", "36"),
634 ("w", "100"),
635 ("h", "20"),
636 ("source", "sprite:button"),
637 ("stretch", "Fill"),
638 ],
639 ),
640 n(
641 "e",
642 &[("x", "76"), ("y", "60"), ("source", "sprite:icon/heart")],
643 ),
644 n("f", &[("x", "90"), ("y", "60"), ("source", "sprite:slot")]),
645 n(
646 "g",
647 &[
648 ("x", "4"),
649 ("y", "72"),
650 ("w", "64"),
651 ("h", "40"),
652 ("source", "item:diamond"),
653 ("stretch", "Fill"),
654 ("tint", "#FF80FF80"),
655 ],
656 ),
657 ];
658 let args = super::super::RenderArgs {
659 path: "/nowhere/src/x.yui".into(),
660 root,
661 screen_w: 200,
662 screen_h: 120,
663 scale: 4,
664 preview: false,
665 };
666 let r = super::super::render(&args).unwrap();
667 let b64 = r
668 .image
669 .trim_start_matches(super::super::raster::PNG_URL_PREFIX);
670 std::fs::write(
671 out,
672 base64::engine::general_purpose::STANDARD
673 .decode(b64)
674 .unwrap(),
675 )
676 .unwrap();
677 eprintln!("assets: {:?}", r.assets);
678 }
679}