diff --git a/packages/lib-roblox/src/datatypes/conversion.rs b/packages/lib-roblox/src/datatypes/conversion.rs index d3333ee..54a6660 100644 --- a/packages/lib-roblox/src/datatypes/conversion.rs +++ b/packages/lib-roblox/src/datatypes/conversion.rs @@ -136,6 +136,9 @@ impl<'lua> RbxVariantToLua<'lua> for LuaAnyUserData<'lua> { Rbx::BrickColor(value) => lua.create_userdata(BrickColor::from(value))?, + Rbx::Color3(value) => lua.create_userdata(Color3::from(value))?, + Rbx::Color3uint8(value) => lua.create_userdata(Color3::from(value))?, + Rbx::UDim(value) => lua.create_userdata(UDim::from(value))?, Rbx::UDim2(value) => lua.create_userdata(UDim2::from(value))?, @@ -168,6 +171,9 @@ impl<'lua> LuaToRbxVariant<'lua> for LuaAnyUserData<'lua> { let f = match variant_type { RbxVariantType::BrickColor => convert::, + RbxVariantType::Color3 => convert::, + RbxVariantType::Color3uint8 => convert::, + RbxVariantType::UDim => convert::, RbxVariantType::UDim2 => convert::, diff --git a/packages/lib-roblox/src/datatypes/types/color3.rs b/packages/lib-roblox/src/datatypes/types/color3.rs new file mode 100644 index 0000000..38093cd --- /dev/null +++ b/packages/lib-roblox/src/datatypes/types/color3.rs @@ -0,0 +1,211 @@ +use core::fmt; + +use glam::Vec3; +use mlua::prelude::*; +use rbx_dom_weak::types::{Color3 as RbxColor3, Color3uint8 as RbxColor3uint8}; + +use super::super::*; + +/** + An implementation of the [Color3](https://create.roblox.com/docs/reference/engine/datatypes/Color3) Roblox datatype. + + This implements all documented properties, methods & constructors of the Color3 class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Color3 { + pub(crate) r: f32, + pub(crate) g: f32, + pub(crate) b: f32, +} + +impl Color3 { + pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { + datatype_table.set( + "new", + lua.create_function(|_, (r, g, b): (Option, Option, Option)| { + Ok(Color3 { + r: r.unwrap_or_default(), + g: g.unwrap_or_default(), + b: b.unwrap_or_default(), + }) + })?, + )?; + datatype_table.set( + "fromRGB", + lua.create_function(|_, (r, g, b): (Option, Option, Option)| { + Ok(Color3 { + r: (r.unwrap_or_default() as f32) / 255f32, + g: (g.unwrap_or_default() as f32) / 255f32, + b: (b.unwrap_or_default() as f32) / 255f32, + }) + })?, + )?; + datatype_table.set( + "fromHSV", + lua.create_function(|_, (h, s, v): (f32, f32, f32)| { + // https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c + let i = (h * 6.0).floor(); + let f = h * 6.0 - i; + let p = v * (1.0 - s); + let q = v * (1.0 - f * s); + let t = v * (1.0 - (1.0 - f) * s); + + let (r, g, b) = match (i % 6.0) as u8 { + 0 => (v, t, p), + 1 => (q, v, p), + 2 => (p, v, t), + 3 => (p, q, v), + 4 => (t, p, v), + 5 => (v, p, q), + _ => unreachable!(), + }; + + Ok(Color3 { r, g, b }) + })?, + )?; + datatype_table.set( + "fromHex", + lua.create_function(|_, hex: String| { + let trimmed = hex.trim_start_matches('#').to_ascii_uppercase(); + let chars = if trimmed.len() == 3 { + ( + u8::from_str_radix(&trimmed[..1].repeat(2), 16), + u8::from_str_radix(&trimmed[1..2].repeat(2), 16), + u8::from_str_radix(&trimmed[2..3].repeat(2), 16), + ) + } else if trimmed.len() == 6 { + ( + u8::from_str_radix(&trimmed[..2], 16), + u8::from_str_radix(&trimmed[2..4], 16), + u8::from_str_radix(&trimmed[4..6], 16), + ) + } else { + return Err(LuaError::RuntimeError(format!( + "Hex color string must be 3 or 6 characters long, got {} character{}", + trimmed.len(), + if trimmed.len() == 1 { "" } else { "s" } + ))); + }; + match chars { + (Ok(r), Ok(g), Ok(b)) => Ok(Color3 { + r: (r as f32) / 255f32, + g: (g as f32) / 255f32, + b: (b as f32) / 255f32, + }), + _ => Err(LuaError::RuntimeError(format!( + "Hex color string '{}' contains invalid character", + trimmed + ))), + } + })?, + )?; + Ok(()) + } +} + +impl LuaUserData for Color3 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("R", |_, this| Ok(this.r)); + fields.add_field_method_get("G", |_, this| Ok(this.g)); + fields.add_field_method_get("B", |_, this| Ok(this.b)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method("Lerp", |_, this, (rhs, alpha): (Color3, f32)| { + let v3_this = Vec3::new(this.r, this.g, this.b); + let v3_rhs = Vec3::new(rhs.r, rhs.g, rhs.b); + let v3 = v3_this.lerp(v3_rhs, alpha); + Ok(Color3 { + r: v3.x, + g: v3.y, + b: v3.z, + }) + }); + methods.add_method("ToHSV", |_, this, ()| { + // https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c + let (r, g, b) = (this.r, this.g, this.b); + let min = r.min(g).min(b); + let max = r.max(g).max(b); + let diff = max - min; + let hue = match max { + max if max == min => 0.0, + max if max == this.r => (g - b) + diff * (if g < b { 6.0 } else { 0.0 }), + max if max == this.g => (b - r) + diff * 2.0, + max if max == this.b => (r - g) + diff * 4.0, + _ => unreachable!(), + }; + let hue = hue / 6.0 * diff; + let sat = if max == 0.0 { 0.0 } else { diff / max }; + let sat = sat.clamp(0.0, 1.0); + Ok((hue, sat, max)) + }); + methods.add_method("ToHex", |_, this, ()| { + Ok(format!( + "{:02X}{:02X}{:02X}", + this.r.clamp(u8::MIN as f32, u8::MAX as f32) as u8, + this.g.clamp(u8::MIN as f32, u8::MAX as f32) as u8, + this.b.clamp(u8::MIN as f32, u8::MAX as f32) as u8, + )) + }); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl Default for Color3 { + fn default() -> Self { + Self { + r: 0f32, + g: 0f32, + b: 0f32, + } + } +} + +impl fmt::Display for Color3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}, {}", self.r, self.g, self.b) + } +} + +impl From for Color3 { + fn from(v: RbxColor3) -> Self { + Self { + r: v.r, + g: v.g, + b: v.b, + } + } +} + +impl From for RbxColor3 { + fn from(v: Color3) -> Self { + Self { + r: v.r, + g: v.g, + b: v.b, + } + } +} + +impl From for Color3 { + fn from(v: RbxColor3uint8) -> Self { + Self { + r: (v.r as f32) / 255f32, + g: (v.g as f32) / 255f32, + b: (v.b as f32) / 255f32, + } + } +} + +impl From for RbxColor3uint8 { + fn from(v: Color3) -> Self { + Self { + r: v.r.clamp(u8::MIN as f32, u8::MAX as f32) as u8, + g: v.g.clamp(u8::MIN as f32, u8::MAX as f32) as u8, + b: v.b.clamp(u8::MIN as f32, u8::MAX as f32) as u8, + } + } +} diff --git a/packages/lib-roblox/src/datatypes/types/mod.rs b/packages/lib-roblox/src/datatypes/types/mod.rs index e33300e..71fca58 100644 --- a/packages/lib-roblox/src/datatypes/types/mod.rs +++ b/packages/lib-roblox/src/datatypes/types/mod.rs @@ -1,4 +1,5 @@ mod brick_color; +mod color3; mod udim; mod udim2; mod vector2; @@ -7,6 +8,7 @@ mod vector3; mod vector3int16; pub use brick_color::BrickColor; +pub use color3::Color3; pub use udim::UDim; pub use udim2::UDim2; pub use vector2::Vector2; diff --git a/packages/lib-roblox/src/lib.rs b/packages/lib-roblox/src/lib.rs index 68c87a5..0323605 100644 --- a/packages/lib-roblox/src/lib.rs +++ b/packages/lib-roblox/src/lib.rs @@ -22,6 +22,7 @@ fn make_all_datatypes(lua: &Lua) -> LuaResult> { use datatypes::types::*; Ok(vec![ ("BrickColor", make_dt(lua, BrickColor::make_table)?), + ("Color3", make_dt(lua, Color3::make_table)?), ("UDim", make_dt(lua, UDim::make_table)?), ("UDim2", make_dt(lua, UDim2::make_table)?), ("Vector2", make_dt(lua, Vector2::make_table)?),