use serde_json::{json, Value};
#[derive(Clone, Copy)]
pub enum PropValue {
Number(f64),
Color(u8, u8, u8),
}
impl PropValue {
pub fn to_json(self) -> Value {
match self {
PropValue::Number(n) => json!(n),
PropValue::Color(r, g, b) => {
json!([f64::from(r) / 255.0, f64::from(g) / 255.0, f64::from(b) / 255.0])
}
}
}
pub fn display(self) -> String {
match self {
PropValue::Number(n) => format!("{n}"),
PropValue::Color(r, g, b) => format!("{r}, {g}, {b}"),
}
}
}
pub struct PropertySpec {
pub name: &'static str,
pub value: PropValue,
}
pub struct InstanceSpec {
pub name: &'static str,
pub class_name: &'static str,
pub parent: Option<&'static str>,
pub properties: &'static [PropertySpec],
}
pub const PLACE_TEMPLATE: &[InstanceSpec] = &[
InstanceSpec {
name: "Lighting",
class_name: "Lighting",
parent: None,
properties: &[
PropertySpec { name: "Ambient", value: PropValue::Color(200, 160, 225) },
PropertySpec { name: "Brightness", value: PropValue::Number(2.5) },
PropertySpec { name: "ColorShift_Bottom", value: PropValue::Color(0, 0, 0) },
PropertySpec { name: "ColorShift_Top", value: PropValue::Color(214, 189, 135) },
PropertySpec { name: "EnvironmentDiffuseScale", value: PropValue::Number(0.5) },
PropertySpec { name: "EnvironmentSpecularScale", value: PropValue::Number(1.0) },
PropertySpec { name: "OutdoorAmbient", value: PropValue::Color(124, 100, 149) },
PropertySpec { name: "FogColor", value: PropValue::Color(200, 170, 249) },
PropertySpec { name: "FogEnd", value: PropValue::Number(2500.0) },
PropertySpec { name: "FogStart", value: PropValue::Number(0.0) },
],
},
InstanceSpec {
name: "ColorCorrection",
class_name: "ColorCorrectionEffect",
parent: Some("Lighting"),
properties: &[
PropertySpec { name: "Brightness", value: PropValue::Number(0.05) },
PropertySpec { name: "Contrast", value: PropValue::Number(0.1) },
PropertySpec { name: "Saturation", value: PropValue::Number(0.15) },
PropertySpec { name: "TintColor", value: PropValue::Color(255, 255, 255) },
],
},
];
pub fn render() -> serde_json::Map<String, Value> {
let mut roots = serde_json::Map::new();
for spec in PLACE_TEMPLATE.iter().filter(|s| s.parent.is_none()) {
let mut node = render_one(spec);
for child in PLACE_TEMPLATE.iter().filter(|s| s.parent == Some(spec.name)) {
node.insert(child.name.to_string(), Value::Object(render_one(child)));
}
roots.insert(spec.name.to_string(), Value::Object(node));
}
roots
}
fn render_one(spec: &InstanceSpec) -> serde_json::Map<String, Value> {
let mut node = serde_json::Map::new();
node.insert("$className".to_string(), json!(spec.class_name));
if !spec.properties.is_empty() {
let mut props = serde_json::Map::new();
for prop in spec.properties {
props.insert(prop.name.to_string(), prop.value.to_json());
}
node.insert("$properties".to_string(), Value::Object(props));
}
node
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn colors_convert_from_studio_0_255_to_rojo_0_1() {
let rendered = render();
let ambient = &rendered["Lighting"]["$properties"]["Ambient"];
let parts: Vec<f64> = ambient.as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
assert!((parts[0] - 200.0 / 255.0).abs() < 1e-9, "got {parts:?}");
assert!((parts[1] - 160.0 / 255.0).abs() < 1e-9, "got {parts:?}");
assert!((parts[2] - 225.0 / 255.0).abs() < 1e-9, "got {parts:?}");
assert!(parts.iter().all(|p| (0.0..=1.0).contains(p)), "out of range: {parts:?}");
}
#[test]
fn children_nest_under_their_declared_parent() {
let rendered = render();
assert!(!rendered.contains_key("ColorCorrection"), "child leaked to top level");
let effect = &rendered["Lighting"]["ColorCorrection"];
assert_eq!(effect["$className"], "ColorCorrectionEffect");
assert_eq!(effect["$properties"]["Contrast"], 0.1);
}
#[test]
fn every_declared_parent_exists() {
for spec in PLACE_TEMPLATE {
if let Some(parent) = spec.parent {
assert!(
PLACE_TEMPLATE.iter().any(|s| s.name == parent && s.parent.is_none()),
"{} declares unknown parent {parent}",
spec.name
);
}
}
}
}