codecraft 0.1.2

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
//! Value types shared by the generated surface model and the document code.

use std::fmt;
use std::str::FromStr;

/// An RGB colour (`color3` in MaterialX).
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Color3 {
    pub r: f32,
    pub g: f32,
    pub b: f32,
}

impl Color3 {
    pub const fn new(r: f32, g: f32, b: f32) -> Self {
        Self { r, g, b }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Vector3 {
    pub x: f32,
    pub y: f32,
    pub z: f32,
}

impl Vector3 {
    pub const fn new(x: f32, y: f32, z: f32) -> Self {
        Self { x, y, z }
    }
}

/// The MaterialX type of an input.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ValueType {
    Float,
    Color3,
    Vector3,
    Boolean,
}

impl ValueType {
    pub const fn mtlx_name(self) -> &'static str {
        match self {
            ValueType::Float => "float",
            ValueType::Color3 => "color3",
            ValueType::Vector3 => "vector3",
            ValueType::Boolean => "boolean",
        }
    }

    pub fn from_mtlx_name(name: &str) -> Option<Self> {
        Some(match name {
            "float" => ValueType::Float,
            "color3" => ValueType::Color3,
            "vector3" => ValueType::Vector3,
            "boolean" => ValueType::Boolean,
            _ => return None,
        })
    }

    /// Parse a MaterialX value string of this type.
    pub fn parse_value(self, text: &str) -> Result<Value, ValueParseError> {
        let err = || ValueParseError {
            ty: self,
            text: text.to_string(),
        };
        Ok(match self {
            ValueType::Float => Value::Float(parse_float(text).ok_or_else(err)?),
            ValueType::Boolean => Value::Boolean(parse_bool(text).ok_or_else(err)?),
            ValueType::Color3 => {
                let [r, g, b] = parse_floats::<3>(text).ok_or_else(err)?;
                Value::Color3(Color3::new(r, g, b))
            }
            ValueType::Vector3 => {
                let [x, y, z] = parse_floats::<3>(text).ok_or_else(err)?;
                Value::Vector3(Vector3::new(x, y, z))
            }
        })
    }
}

impl fmt::Display for ValueType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.mtlx_name())
    }
}

/// A dynamically typed input value.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Value {
    Float(f32),
    Color3(Color3),
    Vector3(Vector3),
    Boolean(bool),
}

impl Value {
    pub const fn value_type(&self) -> ValueType {
        match self {
            Value::Float(_) => ValueType::Float,
            Value::Color3(_) => ValueType::Color3,
            Value::Vector3(_) => ValueType::Vector3,
            Value::Boolean(_) => ValueType::Boolean,
        }
    }

    pub fn to_mtlx_string(&self) -> String {
        match self {
            Value::Float(v) => format_float(*v),
            Value::Boolean(v) => v.to_string(),
            Value::Color3(c) => format!(
                "{}, {}, {}",
                format_float(c.r),
                format_float(c.g),
                format_float(c.b)
            ),
            Value::Vector3(v) => format!(
                "{}, {}, {}",
                format_float(v.x),
                format_float(v.y),
                format_float(v.z)
            ),
        }
    }
}

impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.to_mtlx_string())
    }
}

impl From<f32> for Value {
    fn from(v: f32) -> Self {
        Value::Float(v)
    }
}
impl From<bool> for Value {
    fn from(v: bool) -> Self {
        Value::Boolean(v)
    }
}
impl From<Color3> for Value {
    fn from(v: Color3) -> Self {
        Value::Color3(v)
    }
}
impl From<Vector3> for Value {
    fn from(v: Vector3) -> Self {
        Value::Vector3(v)
    }
}

/// A value string could not be parsed as the requested type.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueParseError {
    pub ty: ValueType,
    pub text: String,
}

impl fmt::Display for ValueParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "cannot parse `{}` as {}", self.text, self.ty)
    }
}

impl std::error::Error for ValueParseError {}

/// Static description of one nodedef input, as declared in the `.mtlx` definition file.
#[derive(Debug, Clone, PartialEq)]
pub struct InputSpec {
    pub name: &'static str,
    pub ty: ValueType,
    /// `None` when the default comes from geometry (see `default_geom_prop`).
    pub default: Option<Value>,
    pub default_geom_prop: Option<&'static str>,
    pub ui_name: &'static str,
    pub ui_folder: &'static str,
    pub ui_min: Option<Value>,
    pub ui_max: Option<Value>,
    pub ui_soft_min: Option<Value>,
    pub ui_soft_max: Option<Value>,
    pub ui_advanced: bool,
    pub uniform: bool,
    pub hint: Option<&'static str>,
    pub doc: &'static str,
}

pub fn parse_float(text: &str) -> Option<f32> {
    f32::from_str(text.trim()).ok()
}

/// Parse a comma- or whitespace-separated list of exactly `N` floats.
pub fn parse_floats<const N: usize>(text: &str) -> Option<[f32; N]> {
    let mut out = [0.0f32; N];
    let mut count = 0;
    let parts = if text.contains(',') {
        text.split(',').collect::<Vec<_>>()
    } else {
        text.split_whitespace().collect::<Vec<_>>()
    };
    for part in parts {
        if count == N {
            return None;
        }
        out[count] = parse_float(part)?;
        count += 1;
    }
    if count != N {
        return None;
    }
    Some(out)
}

pub fn parse_bool(text: &str) -> Option<bool> {
    match text.trim() {
        "true" => Some(true),
        "false" => Some(false),
        _ => None,
    }
}

/// Shortest round-tripping representation, always with a decimal point for finite values.
pub fn format_float(v: f32) -> String {
    let s = format!("{v}");
    if v.is_finite() && !s.contains(['.', 'e', 'E']) {
        format!("{s}.0")
    } else {
        s
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_and_formats_values() {
        assert_eq!(
            ValueType::Float.parse_value(" 1.00 "),
            Ok(Value::Float(1.0))
        );
        assert_eq!(
            ValueType::Color3.parse_value("0.8, 0.8,0.8"),
            Ok(Value::Color3(Color3::new(0.8, 0.8, 0.8)))
        );
        assert_eq!(
            ValueType::Vector3.parse_value("1 2 3"),
            Ok(Value::Vector3(Vector3::new(1.0, 2.0, 3.0)))
        );
        assert_eq!(
            ValueType::Boolean.parse_value("true"),
            Ok(Value::Boolean(true))
        );
        assert!(ValueType::Color3.parse_value("1, 2").is_err());
        assert!(ValueType::Boolean.parse_value("yes").is_err());

        assert_eq!(Value::Float(1.0).to_mtlx_string(), "1.0");
        assert_eq!(Value::Float(0.3).to_mtlx_string(), "0.3");
        assert_eq!(Value::Float(10000.0).to_mtlx_string(), "10000.0");
        assert_eq!(
            Value::Color3(Color3::new(1.0, 0.5, 0.25)).to_mtlx_string(),
            "1.0, 0.5, 0.25"
        );
        assert_eq!(Value::Boolean(false).to_mtlx_string(), "false");
    }
}

/// Error from the generated `get` / `set` / `set_from_str` reflection helpers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputError {
    Unknown(String),
    TypeMismatch {
        name: String,
        expected: ValueType,
        found: ValueType,
    },
    Parse(ValueParseError),
}

impl fmt::Display for InputError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            InputError::Unknown(n) => write!(f, "unknown input `{n}`"),
            InputError::TypeMismatch {
                name,
                expected,
                found,
            } => {
                write!(f, "input `{name}` expects {expected}, got {found}")
            }
            InputError::Parse(e) => write!(f, "{e}"),
        }
    }
}

impl std::error::Error for InputError {}

impl From<ValueParseError> for InputError {
    fn from(e: ValueParseError) -> Self {
        InputError::Parse(e)
    }
}