schematic_mesher/resource_pack/
model.rs

1//! Block model parsing.
2//!
3//! Block models define the 3D geometry of blocks using cuboid elements.
4
5use crate::types::{Direction, ElementRotation};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// A parsed block model from models/*.json.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct BlockModel {
12    /// Parent model to inherit from.
13    #[serde(default)]
14    pub parent: Option<String>,
15
16    /// Whether to use ambient occlusion.
17    #[serde(default = "default_ao", rename = "ambientocclusion")]
18    pub ambient_occlusion: bool,
19
20    /// Texture variable definitions.
21    #[serde(default)]
22    pub textures: HashMap<String, String>,
23
24    /// Model elements (cuboids).
25    #[serde(default)]
26    pub elements: Vec<ModelElement>,
27
28    /// Display transforms (for item rendering, not used for block meshing).
29    #[serde(default)]
30    pub display: Option<serde_json::Value>,
31}
32
33fn default_ao() -> bool {
34    true
35}
36
37impl BlockModel {
38    /// Create an empty model.
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Get the full parent resource location.
44    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    /// Check if this model has its own elements (not inherited).
55    pub fn has_elements(&self) -> bool {
56        !self.elements.is_empty()
57    }
58
59    /// Resolve a texture reference (e.g., "#side") to a texture path.
60    /// Returns None if the reference cannot be resolved.
61    pub fn resolve_texture<'a>(&'a self, reference: &'a str) -> Option<&'a str> {
62        if !reference.starts_with('#') {
63            // Already a direct path
64            return Some(reference);
65        }
66
67        let key = &reference[1..]; // Remove '#'
68        self.textures.get(key).map(|s| s.as_str())
69    }
70}
71
72/// A cuboid element within a model.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ModelElement {
75    /// Minimum corner (0-16 range).
76    pub from: [f32; 3],
77    /// Maximum corner (0-16 range).
78    pub to: [f32; 3],
79    /// Optional rotation.
80    #[serde(default)]
81    pub rotation: Option<ElementRotation>,
82    /// Whether this element receives shade.
83    #[serde(default = "default_shade")]
84    pub shade: bool,
85    /// Face definitions.
86    #[serde(default)]
87    pub faces: HashMap<Direction, ModelFace>,
88}
89
90fn default_shade() -> bool {
91    true
92}
93
94impl ModelElement {
95    /// Get the size of this element in Minecraft coordinates (0-16).
96    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    /// Get the center of this element in Minecraft coordinates.
105    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    /// Convert from Minecraft coordinates (0-16) to normalized (-0.5 to 0.5).
114    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    /// Convert to Minecraft coordinates (0-16) to normalized (-0.5 to 0.5).
123    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    /// Get the normalized center.
132    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    /// Get the normalized size.
138    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    /// Check if this element is very thin (could need double-sided rendering).
144    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/// A face of a model element.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ModelFace {
153    /// UV coordinates [u1, v1, u2, v2] in 0-16 range.
154    #[serde(default)]
155    pub uv: Option<[f32; 4]>,
156    /// Texture reference (e.g., "#side" or "block/stone").
157    pub texture: String,
158    /// Face direction for culling (if adjacent block is opaque, hide this face).
159    #[serde(default)]
160    pub cullface: Option<Direction>,
161    /// UV rotation in degrees (0, 90, 180, 270).
162    #[serde(default)]
163    pub rotation: i32,
164    /// Tint index for biome coloring (-1 = no tint).
165    #[serde(default = "default_tint_index")]
166    pub tintindex: i32,
167}
168
169fn default_tint_index() -> i32 {
170    -1
171}
172
173impl ModelFace {
174    /// Get the UV coordinates, defaulting to full texture if not specified.
175    pub fn uv_or_default(&self) -> [f32; 4] {
176        self.uv.unwrap_or([0.0, 0.0, 16.0, 16.0])
177    }
178
179    /// Get normalized UV coordinates (0-1 range).
180    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    /// Check if this face has a tint.
186    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")); // Only one level
309        assert_eq!(model.resolve_texture("block/dirt"), Some("block/dirt"));
310        assert_eq!(model.resolve_texture("#missing"), None);
311    }
312}