use crate::types::{Direction, ElementRotation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BlockModel {
#[serde(default)]
pub parent: Option<String>,
#[serde(default = "default_ao", rename = "ambientocclusion")]
pub ambient_occlusion: bool,
#[serde(default)]
pub textures: HashMap<String, String>,
#[serde(default)]
pub elements: Vec<ModelElement>,
#[serde(default)]
pub display: Option<serde_json::Value>,
}
fn default_ao() -> bool {
true
}
impl BlockModel {
pub fn new() -> Self {
Self::default()
}
pub fn parent_location(&self) -> Option<String> {
self.parent.as_ref().map(|p| {
if p.contains(':') {
p.clone()
} else {
format!("minecraft:{}", p)
}
})
}
pub fn has_elements(&self) -> bool {
!self.elements.is_empty()
}
pub fn resolve_texture<'a>(&'a self, reference: &'a str) -> Option<&'a str> {
if !reference.starts_with('#') {
return Some(reference);
}
let key = &reference[1..]; self.textures.get(key).map(|s| s.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelElement {
pub from: [f32; 3],
pub to: [f32; 3],
#[serde(default)]
pub rotation: Option<ElementRotation>,
#[serde(default = "default_shade")]
pub shade: bool,
#[serde(default)]
pub faces: HashMap<Direction, ModelFace>,
}
fn default_shade() -> bool {
true
}
impl ModelElement {
pub fn size(&self) -> [f32; 3] {
[
self.to[0] - self.from[0],
self.to[1] - self.from[1],
self.to[2] - self.from[2],
]
}
pub fn center(&self) -> [f32; 3] {
[
(self.from[0] + self.to[0]) / 2.0,
(self.from[1] + self.to[1]) / 2.0,
(self.from[2] + self.to[2]) / 2.0,
]
}
pub fn normalized_from(&self) -> [f32; 3] {
[
self.from[0] / 16.0 - 0.5,
self.from[1] / 16.0 - 0.5,
self.from[2] / 16.0 - 0.5,
]
}
pub fn normalized_to(&self) -> [f32; 3] {
[
self.to[0] / 16.0 - 0.5,
self.to[1] / 16.0 - 0.5,
self.to[2] / 16.0 - 0.5,
]
}
pub fn normalized_center(&self) -> [f32; 3] {
let c = self.center();
[c[0] / 16.0 - 0.5, c[1] / 16.0 - 0.5, c[2] / 16.0 - 0.5]
}
pub fn normalized_size(&self) -> [f32; 3] {
let s = self.size();
[s[0] / 16.0, s[1] / 16.0, s[2] / 16.0]
}
pub fn is_thin(&self, threshold: f32) -> bool {
let size = self.normalized_size();
size[0] < threshold || size[1] < threshold || size[2] < threshold
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelFace {
#[serde(default)]
pub uv: Option<[f32; 4]>,
pub texture: String,
#[serde(default)]
pub cullface: Option<Direction>,
#[serde(default)]
pub rotation: i32,
#[serde(default = "default_tint_index")]
pub tintindex: i32,
}
fn default_tint_index() -> i32 {
-1
}
impl ModelFace {
pub fn uv_or_default(&self) -> [f32; 4] {
self.uv.unwrap_or([0.0, 0.0, 16.0, 16.0])
}
pub fn normalized_uv(&self) -> [f32; 4] {
let uv = self.uv_or_default();
[uv[0] / 16.0, uv[1] / 16.0, uv[2] / 16.0, uv[3] / 16.0]
}
pub fn has_tint(&self) -> bool {
self.tintindex >= 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_model() {
let json = r#"{
"parent": "block/cube_all",
"textures": {
"all": "block/stone"
}
}"#;
let model: BlockModel = serde_json::from_str(json).unwrap();
assert_eq!(model.parent, Some("block/cube_all".to_string()));
assert_eq!(model.textures.get("all"), Some(&"block/stone".to_string()));
assert!(model.elements.is_empty());
}
#[test]
fn test_parse_model_with_elements() {
let json = r##"{
"textures": {
"texture": "block/stone"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"down": { "texture": "#texture", "cullface": "down" },
"up": { "texture": "#texture", "cullface": "up" },
"north": { "texture": "#texture", "cullface": "north" },
"south": { "texture": "#texture", "cullface": "south" },
"west": { "texture": "#texture", "cullface": "west" },
"east": { "texture": "#texture", "cullface": "east" }
}
}
]
}"##;
let model: BlockModel = serde_json::from_str(json).unwrap();
assert_eq!(model.elements.len(), 1);
let element = &model.elements[0];
assert_eq!(element.from, [0.0, 0.0, 0.0]);
assert_eq!(element.to, [16.0, 16.0, 16.0]);
assert_eq!(element.faces.len(), 6);
assert_eq!(
element.faces.get(&Direction::Down).unwrap().cullface,
Some(Direction::Down)
);
}
#[test]
fn test_parse_element_with_rotation() {
let json = r#"{
"from": [0, 0, 0],
"to": [16, 16, 16],
"rotation": {
"origin": [8, 8, 8],
"axis": "y",
"angle": 45,
"rescale": true
},
"faces": {}
}"#;
let element: ModelElement = serde_json::from_str(json).unwrap();
let rotation = element.rotation.unwrap();
assert_eq!(rotation.origin, [8.0, 8.0, 8.0]);
assert_eq!(rotation.angle, 45.0);
assert!(rotation.rescale);
}
#[test]
fn test_element_normalized_coords() {
let element = ModelElement {
from: [0.0, 0.0, 0.0],
to: [16.0, 16.0, 16.0],
rotation: None,
shade: true,
faces: HashMap::new(),
};
assert_eq!(element.normalized_from(), [-0.5, -0.5, -0.5]);
assert_eq!(element.normalized_to(), [0.5, 0.5, 0.5]);
assert_eq!(element.normalized_center(), [0.0, 0.0, 0.0]);
assert_eq!(element.normalized_size(), [1.0, 1.0, 1.0]);
}
#[test]
fn test_face_uv_normalization() {
let face = ModelFace {
uv: Some([0.0, 0.0, 8.0, 8.0]),
texture: "#test".to_string(),
cullface: None,
rotation: 0,
tintindex: -1,
};
assert_eq!(face.normalized_uv(), [0.0, 0.0, 0.5, 0.5]);
}
#[test]
fn test_resolve_texture() {
let model = BlockModel {
textures: [
("all".to_string(), "block/stone".to_string()),
("side".to_string(), "#all".to_string()),
]
.into_iter()
.collect(),
..Default::default()
};
assert_eq!(model.resolve_texture("#all"), Some("block/stone"));
assert_eq!(model.resolve_texture("#side"), Some("#all")); assert_eq!(model.resolve_texture("block/dirt"), Some("block/dirt"));
assert_eq!(model.resolve_texture("#missing"), None);
}
}