use crate::content::refs::RefKind;
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use yog_sync::Arc;
use yog_gfx::image::{
ImageSource, LegacyRegion, SPRITES, Sprite, SpriteSource, Stretch, atlas_texture, fit,
sprite as find_sprite,
};
use yog_gfx::{Color, Rect, Vec2};
use yog_ui::widget::{self, Widget};
use super::assets::{Image, Resources};
use super::markup::{YuiNode, color};
use super::raster::Canvas;
struct Doc {
res: Resources,
mod_id: Option<String>,
}
thread_local! {
static DOC: RefCell<Option<Doc>> = const { RefCell::new(None) };
}
pub(crate) fn set_document(mod_root: Option<&Path>) {
let doc = Doc {
res: Resources::for_mod(mod_root),
mod_id: mod_root.and_then(crate::design::project::mod_id),
};
DOC.with(|d| *d.borrow_mut() = Some(doc));
}
pub(crate) fn resources() -> Resources {
DOC.with(|d| d.borrow().as_ref().map(|d| d.res.clone()))
.unwrap_or_else(|| Resources::for_mod(None))
}
fn with_doc<R>(f: impl FnOnce(Option<&Doc>) -> R) -> R {
DOC.with(|d| f(d.borrow().as_ref()))
}
pub(crate) fn resolve_mod(source: &str) -> String {
let source = source.trim();
match source.strip_prefix("mod:") {
Some(path) => match with_doc(|d| d.and_then(|d| d.mod_id.clone())) {
Some(id) => format!("{id}:{path}"),
None => source.to_string(),
},
None => source.to_string(),
}
}
pub(crate) fn stretch(v: &str) -> Stretch {
Stretch::parse(v).unwrap_or_default()
}
fn uv(v: &str) -> Option<Rect> {
let p: Vec<f32> = v
.split([' ', ','])
.filter(|s| !s.is_empty())
.map(|s| s.parse().ok())
.collect::<Option<_>>()?;
let [x, y, w, h] = <[f32; 4]>::try_from(p).ok()?;
(w > 0.0 && h > 0.0).then(|| Rect::new(x, y, w, h))
}
pub(crate) fn widget(node: &YuiNode) -> Widget {
let a = |k: &str| node.attr(k);
let source = a(yog_ids::yui_attr::SOURCE).unwrap_or_default();
let mut w = widget::image(resolve_mod(source));
if let Some(s) = a(yog_ids::yui_attr::STRETCH) {
w = w.stretch(stretch(s));
}
if let Some(r) = a(yog_ids::yui_attr::UV).and_then(uv) {
w = w.region(r);
}
if let Some(t) = a(yog_ids::yui_attr::TINT).and_then(color) {
w = w.tint(Color(t));
}
w
}
fn video(id: &str) -> Option<Arc<Image>> {
with_doc(|d| d.and_then(|d| d.res.video(id)))
}
fn texture(id: &str) -> Option<Arc<Image>> {
if let Some(sheet) = book_sheet(id) {
return Some(sheet);
}
with_doc(|d| d.and_then(|d| d.res.texture(id)))
}
fn book_sheet(id: &str) -> Option<Arc<Image>> {
static SHEETS: yog_sync::OnceLock<[Option<Arc<Image>>; 2]> = yog_sync::OnceLock::new();
let [book, crafting] = SHEETS.get_or_init(|| {
[
super::assets::decode_png(yog_book::BOOK_PNG).map(Arc::new),
super::assets::decode_png(yog_book::CRAFTING_PNG).map(Arc::new),
]
});
let texture = |sheet: &str| match ImageSource::parse(sheet) {
Some(ImageSource::Texture(t)) => t,
_ => String::new(),
};
match id {
_ if id == texture(yog_book::BOOK_SHEET) => book.clone(),
_ if id == texture(yog_book::CRAFTING_SHEET) => crafting.clone(),
_ => None,
}
}
fn atlas_image(id: &str) -> Option<Arc<Image>> {
let (ns, path) = yog_ids::split(id);
texture(&atlas_texture(ns, path))
}
fn resolve(sp: &Sprite) -> Option<SpriteSource<'_>> {
sp.sources().into_iter().find(|s| match s {
SpriteSource::Atlas(id) => atlas_image(id).is_some(),
SpriteSource::Region(r) => texture(&r.texture).is_some(),
})
}
fn in_game_files(kind: RefKind, id: String) -> String {
let version = with_doc(|d| d.and_then(|d| d.res.vanilla.as_ref().map(|v| v.version.clone())));
version
.and_then(|v| crate::content::refs::equivalent(kind, &id, &v))
.unwrap_or(id)
}
fn item_texture(id: &str, block: bool) -> Option<Arc<Image>> {
if !block
&& let Some(super::item_models::Look::Flat(img)) =
with_doc(|d| d.and_then(|d| super::item_models::look(&d.res, id)))
{
return Some(img);
}
let (ns, name) = yog_ids::split(id);
let item = format!("{ns}:textures/item/{name}.png");
let blocks =
["", "_front", "_side", "_top"].map(|s| format!("{ns}:textures/block/{name}{s}.png"));
let mut order: Vec<&String> = blocks.iter().collect();
if block {
order.push(&item);
} else {
order.insert(0, &item);
}
order
.into_iter()
.find_map(|t| texture(t))
.or_else(|| content_icon(id))
}
fn content_icon(id: &str) -> Option<Arc<Image>> {
type Cache = yog_sync::Mutex<std::collections::HashMap<(PathBuf, String), Option<Arc<Image>>>>;
static CACHE: yog_sync::OnceLock<Cache> = yog_sync::OnceLock::new();
let root = with_doc(|d| d.and_then(|d| d.res.mod_root.clone()))?;
let key = (root.clone(), id.to_string());
let cache = CACHE.get_or_init(Default::default);
if let Some(hit) = cache.lock().get(&key) {
return hit.clone();
}
let img = crate::design::content::content_icon(&root, id)
.and_then(|b| super::assets::decode_png(&b))
.map(Arc::new);
cache.lock().insert(key, img.clone());
img
}
fn item_3d(c: &mut Canvas, id: &str, dest: Rect) -> bool {
match super::item_models::look(c.resources(), id) {
Some(super::item_models::Look::Flat(_)) => false,
Some(super::item_models::Look::Head(faces)) => {
let m = dest.w.min(dest.h) * HEAD_MARGIN;
super::blocks3d::draw_cube(c, faces, dest.inset([m; 4]), SLOT_YAW, SLOT_PITCH);
true
}
None if super::blocks3d::is_block(c.resources(), id) => {
super::blocks3d::draw(c, &format!("0 0 0 {id}"), dest, SLOT_YAW, SLOT_PITCH);
true
}
None => false,
}
}
use yog_book::renderer::{BLOCK_PITCH as SLOT_PITCH, SLOT_YAW};
const HEAD_MARGIN: f32 = 0.1;
pub(crate) fn source_size(source: &str) -> Option<Vec2> {
match ImageSource::parse(source)? {
ImageSource::Item(_) | ImageSource::Block(_) => {
Some(Vec2::new(yog_ui::ICON_SIZE, yog_ui::ICON_SIZE))
}
ImageSource::Sprite(name) => find_sprite(&name).map(|s| match resolve(s) {
Some(SpriteSource::Region(r)) => Vec2::new(r.region.w, r.region.h),
_ => s.size,
}),
ImageSource::Texture(id) if id.ends_with(".svg") => {
svg_document(&id).and_then(|d| yog_media::svg::size(&d))
}
ImageSource::Texture(id) => {
texture(&id).map(|i| Vec2::new(i.width as f32, i.height as f32))
}
ImageSource::Video(id) => video(&id).map(|i| Vec2::new(i.width as f32, i.height as f32)),
ImageSource::ModPath(_) => None,
}
}
fn blit_px(c: &mut Canvas, img: &Image, dest: Rect, src: Rect, t: Color) {
if dest.w <= 0.0 || dest.h <= 0.0 || src.w <= 0.0 || src.h <= 0.0 {
return;
}
let (iw, ih) = (img.width as f32, img.height as f32);
c.blit(
img,
dest,
Rect::new(src.x / iw, src.y / ih, src.w / iw, src.h / ih),
t,
);
}
fn part(c: &mut Canvas, img: Option<Arc<Image>>, dest: Rect, region: Rect, t: Color) {
let Some(img) = img else {
missing(c, dest);
return;
};
let src = if region.w > 0.0 && region.h > 0.0 {
region
} else {
whole(&img)
};
blit_px(c, &img, dest, src, t);
}
fn whole(img: &Image) -> Rect {
Rect::new(0.0, 0.0, img.width as f32, img.height as f32)
}
fn slot(c: &mut Canvas, dest: Rect) {
for (r, color) in yog_ui::inv_slot_bevel(dest) {
c.fill(r, color, color);
}
}
fn sprite(c: &mut Canvas, sp: &Sprite, dest: Rect, part: Rect, t: Color) {
let all = part.w <= 0.0 || part.h <= 0.0;
let mut put = |img: &Image, src: Rect, natural: f32, border: f32| {
if all {
for (to, from) in yog_gfx::image::nine_slice(dest, src, border) {
blit_px(c, img, to, from, t);
}
} else {
let k = src.w / natural;
let from = Rect::new(
src.x + part.x * k,
src.y + part.y * k,
part.w * k,
part.h * k,
);
blit_px(c, img, dest, from, t);
}
};
if sp.is_procedural() {
slot(c, dest);
return;
}
let source = resolve(sp);
if let Some(SpriteSource::Atlas(id)) = &source
&& let Some(img) = atlas_image(id)
{
let size = sp.size;
let frame_h = if img.height > img.width && size.y <= size.x {
img.width as f32 * size.y / size.x
} else {
img.height as f32
};
put(
&img,
Rect::new(0.0, 0.0, img.width as f32, frame_h),
size.x,
sp.border,
);
return;
}
if let Some(SpriteSource::Region(LegacyRegion {
texture: tex,
region,
texture_size,
})) = source
&& let Some(img) = texture(tex)
{
let k = img.width as f32 / texture_size.x;
put(&img, region.scale(k), region.w, sp.border * k);
return;
}
missing(c, dest);
}
fn missing(c: &mut Canvas, dest: Rect) {
let icon = Rect::new(0.0, 0.0, yog_ui::ICON_SIZE, yog_ui::ICON_SIZE);
blit_px(c, &super::assets::missing_image(), dest, icon, Color::WHITE);
}
const SVG_OVERSAMPLE: f32 = 4.0;
fn svg_image(data: &str, w: u32, h: u32) -> Option<Image> {
Some(Image {
width: w,
height: h,
rgba: yog_media::svg::rasterize(data, w, h)?,
})
}
fn svg_document(id: &str) -> Option<String> {
let (ns, path) = yog_ids::split(id);
let bytes = with_doc(|d| d.and_then(|d| d.res.read(ns, path)))?;
String::from_utf8(bytes).ok()
}
pub(crate) fn draw_svg(c: &mut Canvas, data: &str, dest: Rect, t: Color) {
if dest.w <= 0.0 || dest.h <= 0.0 {
return;
}
let side = |v: f32| ((v * SVG_OVERSAMPLE).ceil() as u32).clamp(1, 2048);
match svg_image(data, side(dest.w), side(dest.h)) {
Some(img) => blit_px(c, &img, dest, whole(&img), t),
None => missing(c, dest),
}
}
const ENTITY_EGG_MAX: f32 = 2.0 * yog_ui::ICON_SIZE;
pub(crate) fn draw_entity(c: &mut Canvas, entity: &str, area: Rect) {
let (ns, path) = yog_ids::split(entity);
let side = area.w.min(area.h).min(ENTITY_EGG_MAX);
let dest = Rect::new(
area.x + (area.w - side) / 2.0,
area.y + (area.h - side) / 2.0,
side,
side,
);
let egg = yog_ids::join(ns, &format!("{path}{}", yog_ids::res::SPAWN_EGG_SUFFIX));
draw(
c,
&yog_ids::join(yog_ids::image_scheme::ITEM, &egg),
dest,
Rect::default(),
Color::WHITE,
);
}
pub(crate) fn draw(c: &mut Canvas, source: &str, dest: Rect, region: Rect, t: Color) {
if dest.w <= 0.0 || dest.h <= 0.0 {
return;
}
if let Some(ImageSource::Texture(id)) = ImageSource::parse(source)
&& id.ends_with(".svg")
{
match svg_document(&id) {
Some(doc) => draw_svg(c, &doc, dest, t),
None => missing(c, dest),
}
return;
}
let first_frame =
|img: &Image| Rect::new(0.0, 0.0, img.width as f32, img.width.min(img.height) as f32);
let source = match ImageSource::parse(source) {
Some(ImageSource::Item(id)) => Some(ImageSource::Item(in_game_files(RefKind::Item, id))),
Some(ImageSource::Block(id)) => Some(ImageSource::Block(in_game_files(RefKind::Block, id))),
other => other,
};
match source {
Some(ImageSource::Item(id)) if item_3d(c, &id, dest) => {}
Some(ImageSource::Item(id)) => match item_texture(&id, false) {
Some(img) => blit_px(c, &img, dest, first_frame(&img), Color::WHITE),
None => missing(c, dest),
},
Some(ImageSource::Block(id)) => match item_texture(&id, true) {
Some(img) => blit_px(c, &img, dest, first_frame(&img), Color::WHITE),
None => missing(c, dest),
},
Some(ImageSource::Sprite(name)) => match find_sprite(&name) {
Some(sp) => sprite(c, sp, dest, region, t),
None => match name
.split_once(':')
.and_then(|(ns, p)| texture(&atlas_texture(ns, p)))
{
Some(img) => blit_px(c, &img, dest, whole(&img), t),
None => missing(c, dest),
},
},
Some(ImageSource::Texture(id)) => part(c, texture(&id), dest, region, t),
Some(ImageSource::Video(id)) => part(c, video(&id), dest, region, t),
Some(ImageSource::ModPath(_)) | None => missing(c, dest),
}
}
#[derive(Debug, Clone, yog_data::Schema)]
pub(crate) struct Candidate {
pub(crate) source: String,
pub(crate) group: String,
pub(crate) detail: String,
}
pub(crate) fn candidates(path: &Path) -> Vec<Candidate> {
let root = super::mod_root(path);
let mut out = Vec::new();
if let Some(root) = &root {
out.extend(crate::media::pictures(root).into_iter().map(|p| Candidate {
source: ImageSource::ModPath(p.path()).to_string(),
group: "mod".into(),
detail: format!("@{}.{}", p.set, p.name),
}));
}
out.extend(SPRITES.iter().map(|s| Candidate {
source: format!("sprite:{}", s.name),
group: "sprite".into(),
detail: format!("{} ({}×{})", s.description, s.size.x, s.size.y),
}));
let res = Resources::for_mod(root.as_deref());
if let Some(v) = &res.vanilla {
let (items, blocks) = v.texture_names();
out.extend(items.into_iter().map(|n| Candidate {
source: yog_ids::join(yog_ids::image_scheme::ITEM, &n),
group: yog_ids::image_scheme::ITEM.into(),
detail: String::new(),
}));
out.extend(blocks.into_iter().map(|n| Candidate {
source: yog_ids::join(yog_ids::image_scheme::BLOCK, &n),
group: yog_ids::image_scheme::BLOCK.into(),
detail: String::new(),
}));
}
out
}
#[derive(yog_data::Schema)]
pub(crate) struct Preview {
pub(crate) url: Option<String>,
pub(crate) alias: Option<String>,
}
pub(crate) fn preview(path: &Path, source: &str, size: u32) -> Preview {
let alias = yog_gfx::image::suggest_alias(source);
let url = draw_preview(path, alias.as_deref().unwrap_or(source), size);
Preview { url, alias }
}
const MIN_PREVIEW: u32 = 8;
const MAX_PREVIEW: u32 = 256;
const ICON_SIDE: f32 = 16.0;
fn draw_preview(path: &Path, source: &str, size: u32) -> Option<String> {
let root = super::mod_root(path);
set_document(root.as_deref());
let res = Resources::for_mod(root.as_deref());
let source = resolve_mod(source);
let natural = source_size(&source).unwrap_or(Vec2::new(ICON_SIDE, ICON_SIDE));
let size = size.clamp(MIN_PREVIEW, MAX_PREVIEW);
let (dest, _) = fit(
Stretch::Uniform,
natural,
Rect::new(0.0, 0.0, size as f32, size as f32),
);
let mut c = Canvas::new(size, size, 1, &res);
draw(&mut c, &source, dest, Rect::default(), Color::WHITE);
c.png_url()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore]
fn sample_png() {
use base64::Engine as _;
let Ok(out) = std::env::var(yog_ids::env::IMAGES_PNG) else {
return;
};
let n = |uid: &str, attrs: &[(&str, &str)]| YuiNode {
uid: uid.into(),
kind: "Image".into(),
attrs: attrs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
children: vec![],
};
let mut root = n(
"s",
&[("width", "200"), ("height", "120"), ("bg", "#C0101010")],
);
root.kind = "Screen".into();
root.children = vec![
n(
"a",
&[
("x", "4"),
("y", "4"),
("w", "156"),
("h", "29"),
("source", "minecraft:item/apple"),
("bg", "#40FFFFFF"),
],
),
n(
"b",
&[
("x", "4"),
("y", "36"),
("w", "32"),
("h", "32"),
("source", "item:apple"),
],
),
n(
"c",
&[
("x", "40"),
("y", "36"),
("w", "32"),
("h", "32"),
("source", "block:crafting_table"),
],
),
n(
"d",
&[
("x", "76"),
("y", "36"),
("w", "100"),
("h", "20"),
("source", "sprite:button"),
("stretch", "Fill"),
],
),
n(
"e",
&[("x", "76"), ("y", "60"), ("source", "sprite:icon/heart")],
),
n("f", &[("x", "90"), ("y", "60"), ("source", "sprite:slot")]),
n(
"g",
&[
("x", "4"),
("y", "72"),
("w", "64"),
("h", "40"),
("source", "item:diamond"),
("stretch", "Fill"),
("tint", "#FF80FF80"),
],
),
];
let args = super::super::RenderArgs {
path: "/nowhere/src/x.yui".into(),
root,
screen_w: 200,
screen_h: 120,
scale: 4,
preview: false,
};
let r = super::super::render(&args).unwrap();
let b64 = r
.image
.trim_start_matches(super::super::raster::PNG_URL_PREFIX);
std::fs::write(
out,
base64::engine::general_purpose::STANDARD
.decode(b64)
.unwrap(),
)
.unwrap();
}
}