use std::fmt;
use std::str::FromStr;
#[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 }
}
}
#[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,
})
}
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())
}
}
#[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)
}
}
#[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 {}
#[derive(Debug, Clone, PartialEq)]
pub struct InputSpec {
pub name: &'static str,
pub ty: ValueType,
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()
}
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,
}
}
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");
}
}
#[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)
}
}