yog_cli/design/content/
texture.rs1use serde_json::Value;
5use std::path::{Path, PathBuf};
6
7use super::{texture_bytes, texture_file, texture_id};
8use crate::design::project::mod_root;
9use crate::serve::ServiceError;
10
11#[derive(yog_data::Schema)]
12pub struct Texture {
13 pub url: String,
15 pub animation: Option<Animation>,
17 pub writable: bool,
20}
21
22#[derive(Clone, Debug, PartialEq, yog_data::Schema)]
26#[data(rename_all = "camelCase")]
27pub struct Animation {
28 pub frame_width: u32,
29 pub frame_height: u32,
30 pub frames: Vec<AnimationFrame>,
31 pub interpolate: bool,
33}
34
35#[derive(Clone, Debug, PartialEq, yog_data::Schema)]
36pub struct AnimationFrame {
37 pub index: u32,
38 pub time: u32,
40}
41
42impl Animation {
43 pub fn read(meta: &str, size: (u32, u32)) -> Option<Animation> {
45 let doc: Value = serde_json::from_str(meta).ok()?;
46 Self::of(&yog_formats::texture::Animation::from_mcmeta(&doc)?, size)
47 }
48
49 pub fn of(a: &yog_formats::texture::Animation, (w, h): (u32, u32)) -> Option<Animation> {
52 let side = w.min(h);
53 let fw = a.width.unwrap_or(side).max(1);
54 let fh = a.height.unwrap_or(side).max(1);
55 let count = (w / fw) * (h / fh);
56 if count == 0 {
57 return None;
58 }
59 let frame_time = a.frame_time().max(1);
60 let frames = if a.frames.is_empty() {
61 (0..count)
62 .map(|index| AnimationFrame {
63 index,
64 time: frame_time,
65 })
66 .collect()
67 } else {
68 a.frames
69 .iter()
70 .filter(|f| f.index < count)
71 .map(|f| AnimationFrame {
72 index: f.index,
73 time: f.time.map_or(frame_time, |t| t.max(1)),
74 })
75 .collect()
76 };
77 Some(Animation {
78 frame_width: fw,
79 frame_height: fh,
80 frames,
81 interpolate: a.interpolate,
82 })
83 }
84
85 pub fn to_format(&self, (w, h): (u32, u32)) -> yog_formats::texture::Animation {
87 let side = w.min(h);
88 yog_formats::texture::Animation {
89 frame_time: None,
90 interpolate: self.interpolate,
91 frames: self
92 .frames
93 .iter()
94 .map(|f| yog_formats::texture::Frame {
95 index: f.index,
96 time: Some(f.time),
97 })
98 .collect(),
99 width: (self.frame_width != side).then_some(self.frame_width),
100 height: (self.frame_height != side).then_some(self.frame_height),
101 }
102 }
103}
104
105pub fn write_animation(
108 file: &Path,
109 texture: &str,
110 animation: Option<&Animation>,
111) -> Result<PathBuf, String> {
112 let root = mod_root(file).ok_or("not in a mod")?;
113 let id = texture_id(texture);
114 let animation = match animation {
115 Some(a) => {
116 let png = texture_bytes(file, texture).map_err(|e| e.message)?;
117 let size = image_size(&png).ok_or_else(|| format!("{id} isn't a picture"))?;
118 Some(a.to_format(size))
119 }
120 None => None,
121 };
122 crate::media::save_texture_animation(&root, &id, animation)
123}
124
125pub fn body_texture(file: &Path, texture: &str) -> Result<Texture, ServiceError> {
127 self::texture(file, texture)
128}
129
130pub fn texture(file: &Path, texture: &str) -> Result<Texture, ServiceError> {
131 let bytes = texture_bytes(file, texture)?;
132 let animation = image_size(&bytes).and_then(|size| {
133 let meta = texture_file(file, texture, "png.mcmeta").ok()?;
134 Animation::read(&String::from_utf8_lossy(&meta), size)
135 });
136 Ok(Texture {
137 url: super::super::raster::png_url(&bytes),
138 animation,
139 writable: own_texture(file, texture).is_some(),
140 })
141}
142
143pub fn own_texture(file: &Path, texture: &str) -> Option<(PathBuf, String)> {
147 if texture.starts_with(yog_formats::content::SLOT_MARK) {
148 return None;
149 }
150 let root = mod_root(file)?;
151 let id = texture_id(texture);
152 crate::media::is_own_texture(&root, &id).then_some((root, id))
153}
154
155pub fn write_texture(file: &Path, texture: &str, png: Vec<u8>) -> Result<PathBuf, String> {
157 if image_size(&png).is_none() {
158 return Err("not a PNG image".into());
159 }
160 let (root, id) = own_texture(file, texture)
161 .ok_or_else(|| format!("{texture} isn't a texture of this mod"))?;
162 crate::media::save_texture_picture(&root, &id, png)
163}
164
165pub(super) fn image_size(png: &[u8]) -> Option<(u32, u32)> {
167 if png.len() < 24 || &png[1..4] != b"PNG" {
168 return None;
169 }
170 let w = u32::from_be_bytes(png[16..20].try_into().ok()?);
171 let h = u32::from_be_bytes(png[20..24].try_into().ok()?);
172 Some((w, h))
173}