schematic_mesher/resource_pack/
model.rs1use crate::types::{Direction, ElementRotation};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct BlockModel {
12 #[serde(default)]
14 pub parent: Option<String>,
15
16 #[serde(default = "default_ao", rename = "ambientocclusion")]
18 pub ambient_occlusion: bool,
19
20 #[serde(default)]
22 pub textures: HashMap<String, String>,
23
24 #[serde(default)]
26 pub elements: Vec<ModelElement>,
27
28 #[serde(default)]
30 pub display: Option<serde_json::Value>,
31}
32
33fn default_ao() -> bool {
34 true
35}
36
37impl BlockModel {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn parent_location(&self) -> Option<String> {
45 self.parent.as_ref().map(|p| {
46 if p.contains(':') {
47 p.clone()
48 } else {
49 format!("minecraft:{}", p)
50 }
51 })
52 }
53
54 pub fn has_elements(&self) -> bool {
56 !self.elements.is_empty()
57 }
58
59 pub fn resolve_texture<'a>(&'a self, reference: &'a str) -> Option<&'a str> {
62 if !reference.starts_with('#') {
63 return Some(reference);
65 }
66
67 let key = &reference[1..]; self.textures.get(key).map(|s| s.as_str())
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ModelElement {
75 pub from: [f32; 3],
77 pub to: [f32; 3],
79 #[serde(default)]
81 pub rotation: Option<ElementRotation>,
82 #[serde(default = "default_shade")]
84 pub shade: bool,
85 #[serde(default)]
87 pub faces: HashMap<Direction, ModelFace>,
88}
89
90fn default_shade() -> bool {
91 true
92}
93
94impl ModelElement {
95 pub fn size(&self) -> [f32; 3] {
97 [
98 self.to[0] - self.from[0],
99 self.to[1] - self.from[1],
100 self.to[2] - self.from[2],
101 ]
102 }
103
104 pub fn center(&self) -> [f32; 3] {
106 [
107 (self.from[0] + self.to[0]) / 2.0,
108 (self.from[1] + self.to[1]) / 2.0,
109 (self.from[2] + self.to[2]) / 2.0,
110 ]
111 }
112
113 pub fn normalized_from(&self) -> [f32; 3] {
115 [
116 self.from[0] / 16.0 - 0.5,
117 self.from[1] / 16.0 - 0.5,
118 self.from[2] / 16.0 - 0.5,
119 ]
120 }
121
122 pub fn normalized_to(&self) -> [f32; 3] {
124 [
125 self.to[0] / 16.0 - 0.5,
126 self.to[1] / 16.0 - 0.5,
127 self.to[2] / 16.0 - 0.5,
128 ]
129 }
130
131 pub fn normalized_center(&self) -> [f32; 3] {
133 let c = self.center();
134 [c[0] / 16.0 - 0.5, c[1] / 16.0 - 0.5, c[2] / 16.0 - 0.5]
135 }
136
137 pub fn normalized_size(&self) -> [f32; 3] {
139 let s = self.size();
140 [s[0] / 16.0, s[1] / 16.0, s[2] / 16.0]
141 }
142
143 pub fn is_thin(&self, threshold: f32) -> bool {
145 let size = self.normalized_size();
146 size[0] < threshold || size[1] < threshold || size[2] < threshold
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ModelFace {
153 #[serde(default)]
155 pub uv: Option<[f32; 4]>,
156 pub texture: String,
158 #[serde(default)]
160 pub cullface: Option<Direction>,
161 #[serde(default)]
163 pub rotation: i32,
164 #[serde(default = "default_tint_index")]
166 pub tintindex: i32,
167}
168
169fn default_tint_index() -> i32 {
170 -1
171}
172
173impl ModelFace {
174 pub fn uv_or_default(&self) -> [f32; 4] {
176 self.uv.unwrap_or([0.0, 0.0, 16.0, 16.0])
177 }
178
179 pub fn normalized_uv(&self) -> [f32; 4] {
181 let uv = self.uv_or_default();
182 [uv[0] / 16.0, uv[1] / 16.0, uv[2] / 16.0, uv[3] / 16.0]
183 }
184
185 pub fn has_tint(&self) -> bool {
187 self.tintindex >= 0
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_parse_simple_model() {
197 let json = r#"{
198 "parent": "block/cube_all",
199 "textures": {
200 "all": "block/stone"
201 }
202 }"#;
203
204 let model: BlockModel = serde_json::from_str(json).unwrap();
205 assert_eq!(model.parent, Some("block/cube_all".to_string()));
206 assert_eq!(model.textures.get("all"), Some(&"block/stone".to_string()));
207 assert!(model.elements.is_empty());
208 }
209
210 #[test]
211 fn test_parse_model_with_elements() {
212 let json = r##"{
213 "textures": {
214 "texture": "block/stone"
215 },
216 "elements": [
217 {
218 "from": [0, 0, 0],
219 "to": [16, 16, 16],
220 "faces": {
221 "down": { "texture": "#texture", "cullface": "down" },
222 "up": { "texture": "#texture", "cullface": "up" },
223 "north": { "texture": "#texture", "cullface": "north" },
224 "south": { "texture": "#texture", "cullface": "south" },
225 "west": { "texture": "#texture", "cullface": "west" },
226 "east": { "texture": "#texture", "cullface": "east" }
227 }
228 }
229 ]
230 }"##;
231
232 let model: BlockModel = serde_json::from_str(json).unwrap();
233 assert_eq!(model.elements.len(), 1);
234
235 let element = &model.elements[0];
236 assert_eq!(element.from, [0.0, 0.0, 0.0]);
237 assert_eq!(element.to, [16.0, 16.0, 16.0]);
238 assert_eq!(element.faces.len(), 6);
239 assert_eq!(
240 element.faces.get(&Direction::Down).unwrap().cullface,
241 Some(Direction::Down)
242 );
243 }
244
245 #[test]
246 fn test_parse_element_with_rotation() {
247 let json = r#"{
248 "from": [0, 0, 0],
249 "to": [16, 16, 16],
250 "rotation": {
251 "origin": [8, 8, 8],
252 "axis": "y",
253 "angle": 45,
254 "rescale": true
255 },
256 "faces": {}
257 }"#;
258
259 let element: ModelElement = serde_json::from_str(json).unwrap();
260 let rotation = element.rotation.unwrap();
261 assert_eq!(rotation.origin, [8.0, 8.0, 8.0]);
262 assert_eq!(rotation.angle, 45.0);
263 assert!(rotation.rescale);
264 }
265
266 #[test]
267 fn test_element_normalized_coords() {
268 let element = ModelElement {
269 from: [0.0, 0.0, 0.0],
270 to: [16.0, 16.0, 16.0],
271 rotation: None,
272 shade: true,
273 faces: HashMap::new(),
274 };
275
276 assert_eq!(element.normalized_from(), [-0.5, -0.5, -0.5]);
277 assert_eq!(element.normalized_to(), [0.5, 0.5, 0.5]);
278 assert_eq!(element.normalized_center(), [0.0, 0.0, 0.0]);
279 assert_eq!(element.normalized_size(), [1.0, 1.0, 1.0]);
280 }
281
282 #[test]
283 fn test_face_uv_normalization() {
284 let face = ModelFace {
285 uv: Some([0.0, 0.0, 8.0, 8.0]),
286 texture: "#test".to_string(),
287 cullface: None,
288 rotation: 0,
289 tintindex: -1,
290 };
291
292 assert_eq!(face.normalized_uv(), [0.0, 0.0, 0.5, 0.5]);
293 }
294
295 #[test]
296 fn test_resolve_texture() {
297 let model = BlockModel {
298 textures: [
299 ("all".to_string(), "block/stone".to_string()),
300 ("side".to_string(), "#all".to_string()),
301 ]
302 .into_iter()
303 .collect(),
304 ..Default::default()
305 };
306
307 assert_eq!(model.resolve_texture("#all"), Some("block/stone"));
308 assert_eq!(model.resolve_texture("#side"), Some("#all")); assert_eq!(model.resolve_texture("block/dirt"), Some("block/dirt"));
310 assert_eq!(model.resolve_texture("#missing"), None);
311 }
312}