use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use yog_gfx::{Color, Rect, Vec2};
use yog_gfx::image::{
ImageSource, LegacyRegion, SPRITES, Sprite, SpriteSource, Stretch, atlas_texture, fit, sprite as find_sprite,
};
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 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 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 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 fn stretch(v: &str) -> Stretch {
match v.trim().to_ascii_lowercase().as_str() {
"none" => Stretch::None,
"fill" => Stretch::Fill,
"uniformtofill" | "uniform_to_fill" => Stretch::UniformToFill,
_ => Stretch::Uniform,
}
}
fn uv(v: &str) -> Option<[f32; 4]> {
let p: Vec<f32> = v
.split([' ', ','])
.filter(|s| !s.is_empty())
.map(|s| s.parse().ok())
.collect::<Option<_>>()?;
<[f32; 4]>::try_from(p)
.ok()
.filter(|r| r[2] > 0.0 && r[3] > 0.0)
}
fn rect([x, y, w, h]: [f32; 4]) -> Rect {
Rect::new(x, y, w, h)
}
pub 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(rect(r));
}
if let Some(t) = a(yog_ids::yui_attr::TINT).and_then(color) {
w = w.tint(Color(t));
}
w
}
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: std::sync::OnceLock<[Option<Arc<Image>>; 2]> = std::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 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 = std::sync::Mutex<std::collections::HashMap<(PathBuf, String), Option<Arc<Image>>>>;
static CACHE: std::sync::OnceLock<Cache> = std::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().ok()?.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().ok()?.insert(key, img.clone());
img
}
fn item_3d(c: &mut Canvas, id: &str, [x, y, w, h]: [f32; 4]) -> 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 = w.min(h) * HEAD_MARGIN;
super::blocks3d::draw_cube(c, faces, [x + m, y + m, w - 2.0 * m, h - 2.0 * m], SLOT_YAW, SLOT_PITCH);
true
}
None if super::blocks3d::is_block(c.resources(), id) => {
super::blocks3d::draw(c, &format!("0 0 0 {id}"), [x, y, w, h], 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 fn source_size(source: &str) -> Option<(f32, f32)> {
match ImageSource::parse(source)? {
ImageSource::Item(_) | ImageSource::Block(_) => Some((yog_ui::ICON_SIZE, yog_ui::ICON_SIZE)),
ImageSource::Sprite(name) => find_sprite(&name).map(|s| match resolve(s) {
Some(SpriteSource::Region(r)) => (r.region.w, r.region.h),
_ => (s.size.x, s.size.y),
}),
ImageSource::Texture(id) if id.ends_with(".svg") => svg_document(&id).and_then(|d| svg_size(&d)),
ImageSource::Texture(id) => texture(&id).map(|i| (i.width as f32, i.height as f32)),
ImageSource::ModPath(_) => None,
}
}
fn tint(c: u32) -> [f32; 4] {
Color(c).to_f32()
}
fn blit_px(c: &mut Canvas, img: &Image, dest: [f32; 4], src: [f32; 4], t: u32) {
let [x, y, w, h] = dest;
let [u, v, uw, vh] = src;
if w <= 0.0 || h <= 0.0 || uw <= 0.0 || vh <= 0.0 {
return;
}
let (iw, ih) = (img.width as f32, img.height as f32);
c.blit(
img,
x,
y,
w,
h,
[u / iw, v / ih, (u + uw) / iw, (v + vh) / ih],
tint(t),
);
}
fn nine_slice(c: &mut Canvas, img: &Image, region: [f32; 4], border: f32, dest: [f32; 4], t: u32) {
let [x, y, w, h] = dest;
let [u, v, rw, rh] = region;
let b = border.min(rw / 2.0).min(rh / 2.0).min(w / 2.0).min(h / 2.0);
if b <= 0.0 || (w == rw && h == rh) {
blit_px(c, img, dest, region, t);
return;
}
let cols = [
(x, b, u, b),
(x + b, w - 2.0 * b, u + b, rw - 2.0 * b),
(x + w - b, b, u + rw - b, b),
];
let rows = [
(y, b, v, b),
(y + b, h - 2.0 * b, v + b, rh - 2.0 * b),
(y + h - b, b, v + rh - b, b),
];
for (dy, dh, sv, sh) in rows {
for (dx, dw, su, sw) in cols {
blit_px(c, img, [dx, dy, dw, dh], [su, sv, sw, sh], t);
}
}
}
fn slot(c: &mut Canvas, [x, y, w, h]: [f32; 4]) {
let (face, shadow, light) = (yog_ui::INV_SLOT_FACE.0, yog_ui::INV_SLOT_SHADOW.0, yog_ui::INV_SLOT_LIGHT.0);
c.fill(x, y, x + w, y + h, face, face);
c.fill(x, y, x + w, y + 1.0, shadow, shadow);
c.fill(x, y, x + 1.0, y + h, shadow, shadow);
c.fill(x, y + h - 1.0, x + w, y + h, light, light);
c.fill(x + w - 1.0, y, x + w, y + h, light, light);
}
fn sprite(c: &mut Canvas, sp: &Sprite, dest: [f32; 4], part: [f32; 4], t: u32) {
let whole = part[2] <= 0.0 || part[3] <= 0.0;
let mut put = |img: &Image, src: [f32; 4], natural: f32, border: f32| {
if whole {
nine_slice(c, img, src, border, dest, t);
} else {
let k = src[2] / natural;
blit_px(c, img, dest, [src[0] + part[0] * k, src[1] + part[1] * k, part[2] * k, part[3] * k], 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, [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.x * k, region.y * k, region.w * k, region.h * k], region.w, sp.border * k);
return;
}
missing(c, dest);
}
fn missing(c: &mut Canvas, dest: [f32; 4]) {
blit_px(
c,
&super::assets::missing_image(),
dest,
[0.0, 0.0, yog_ui::ICON_SIZE, yog_ui::ICON_SIZE],
Color::WHITE.0,
);
}
const SVG_OVERSAMPLE: f32 = 4.0;
fn svg_image(data: &str, w: u32, h: u32) -> Option<Image> {
let tree = resvg::usvg::Tree::from_str(data, &resvg::usvg::Options::default()).ok()?;
let mut pixmap = resvg::tiny_skia::Pixmap::new(w, h)?;
let size = tree.size();
let t = resvg::tiny_skia::Transform::from_scale(w as f32 / size.width(), h as f32 / size.height());
resvg::render(&tree, t, &mut pixmap.as_mut());
let mut rgba = pixmap.take();
for p in rgba.as_chunks_mut::<4>().0 {
let a = p[3] as u32;
if a > 0 && a < 255 {
for c in &mut p[..3] {
*c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
}
}
}
Some(Image { width: w, height: h, rgba })
}
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()
}
fn svg_size(data: &str) -> Option<(f32, f32)> {
let tree = resvg::usvg::Tree::from_str(data, &resvg::usvg::Options::default()).ok()?;
Some((tree.size().width(), tree.size().height()))
}
pub fn draw_svg(c: &mut Canvas, data: &str, dest: [f32; 4], t: u32) {
if dest[2] <= 0.0 || dest[3] <= 0.0 {
return;
}
let side = |v: f32| ((v * SVG_OVERSAMPLE).ceil() as u32).clamp(1, 2048);
match svg_image(data, side(dest[2]), side(dest[3])) {
Some(img) => blit_px(c, &img, dest, [0.0, 0.0, img.width as f32, img.height as f32], t),
None => missing(c, dest),
}
}
const ENTITY_EGG_MAX: f32 = 2.0 * yog_ui::ICON_SIZE;
pub fn draw_entity(c: &mut Canvas, entity: &str, [x, y, w, h]: [f32; 4]) {
let (ns, path) = yog_ids::split(entity);
let side = w.min(h).min(ENTITY_EGG_MAX);
let dest = [x + (w - side) / 2.0, y + (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, [0.0; 4], Color::WHITE.0);
}
pub fn draw(c: &mut Canvas, source: &str, dest: [f32; 4], region: [f32; 4], t: u32) {
if dest[2] <= 0.0 || dest[3] <= 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| [0.0, 0.0, img.width as f32, img.width.min(img.height) as f32];
match ImageSource::parse(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.0),
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.0),
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, [0.0, 0.0, img.width as f32, img.height as f32], t),
None => missing(c, dest),
},
},
Some(ImageSource::Texture(id)) => match texture(&id) {
Some(img) => {
let src = if region[2] > 0.0 && region[3] > 0.0 {
region
} else {
[0.0, 0.0, img.width as f32, img.height as f32]
};
blit_px(c, &img, dest, src, t);
}
None => missing(c, dest),
},
Some(ImageSource::ModPath(_)) | None => missing(c, dest),
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Candidate {
pub source: String,
pub group: String,
pub detail: String,
}
pub 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(serde::Serialize)]
pub struct Preview {
pub url: Option<String>,
pub alias: Option<String>,
}
pub 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> {
use base64::Engine as _;
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 (nw, nh) = source_size(&source).unwrap_or((ICON_SIDE, ICON_SIDE));
let size = size.clamp(MIN_PREVIEW, MAX_PREVIEW);
let (dest, _) = fit(Stretch::Uniform, Vec2::new(nw, nh), Rect::new(0.0, 0.0, size as f32, size as f32));
let mut c = Canvas::new(size, size, 1, &res);
let [x, y, w, h] = [dest.x, dest.y, dest.w, dest.h];
draw(&mut c, &source, [x, y, w, h], [0.0; 4], Color::WHITE.0);
let png = c.png().ok()?;
Some(format!(
"data:image/png;base64,{}",
base64::engine::general_purpose::STANDARD.encode(png)
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore]
fn sample_png() {
use base64::Engine as _;
let Ok(out) = std::env::var("YOG_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("data:image/png;base64,");
std::fs::write(
out,
base64::engine::general_purpose::STANDARD
.decode(b64)
.unwrap(),
)
.unwrap();
eprintln!("assets: {:?}", r.assets);
}
}