diff --git a/crates/processing_pyo3/mewnala/__init__.py b/crates/processing_pyo3/mewnala/__init__.py index 77477b9..7fe6f79 100644 --- a/crates/processing_pyo3/mewnala/__init__.py +++ b/crates/processing_pyo3/mewnala/__init__.py @@ -1 +1,12 @@ from .mewnala import * + +# re-export the native submodules as submodules of this module, if they exist +# this allows users to import from `mewnala.math` and `mewnala.color` +# if they exist, without needing to know about the internal structure of the native module +import sys as _sys +from . import mewnala as _native +for _name in ("math", "color"): + _sub = getattr(_native, _name, None) + if _sub is not None: + _sys.modules[f"{__name__}.{_name}"] = _sub +del _sys, _native, _name, _sub diff --git a/crates/processing_pyo3/src/color.rs b/crates/processing_pyo3/src/color.rs new file mode 100644 index 0000000..3d43cbc --- /dev/null +++ b/crates/processing_pyo3/src/color.rs @@ -0,0 +1,556 @@ +use bevy::color::{ + Alpha, Color, Gray, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, Luminance, Mix, Oklaba, + Oklcha, Saturation, Srgba, Xyza, color_difference::EuclideanDistance, +}; +use pyo3::{exceptions::PyTypeError, prelude::*, types::PyTuple}; + +use crate::math::{PyVec4, PyVecIter, hash_f32}; + +#[pyclass(name = "Color", from_py_object)] +#[derive(Clone, Debug)] +pub struct PyColor(pub(crate) Color); + +impl From for PyColor { + fn from(c: Color) -> Self { + Self(c) + } +} + +impl From for Color { + fn from(c: PyColor) -> Self { + c.0 + } +} + +impl From for PyColor { + fn from(c: Srgba) -> Self { + Self(Color::Srgba(c)) + } +} + +fn extract_component(obj: &Bound<'_, PyAny>) -> PyResult { + if let Ok(v) = obj.extract::() { + return Ok(v as f32 / 255.0); + } + if let Ok(v) = obj.extract::() { + return Ok(v as f32); + } + Err(PyTypeError::new_err("expected int or float")) +} + +fn to_srgba(color: &Color) -> Srgba { + color.to_srgba() +} + +fn components(color: &Color) -> [f32; 4] { + use bevy::color::ColorToComponents; + match *color { + Color::Srgba(c) => c.to_f32_array(), + Color::LinearRgba(c) => c.to_f32_array(), + Color::Hsla(c) => c.to_f32_array(), + Color::Hsva(c) => c.to_f32_array(), + Color::Hwba(c) => c.to_f32_array(), + Color::Laba(c) => c.to_f32_array(), + Color::Lcha(c) => c.to_f32_array(), + Color::Oklaba(c) => c.to_f32_array(), + Color::Oklcha(c) => c.to_f32_array(), + Color::Xyza(c) => c.to_f32_array(), + } +} + +fn components_no_alpha(color: &Color) -> [f32; 3] { + use bevy::color::ColorToComponents; + match *color { + Color::Srgba(c) => c.to_f32_array_no_alpha(), + Color::LinearRgba(c) => c.to_f32_array_no_alpha(), + Color::Hsla(c) => c.to_f32_array_no_alpha(), + Color::Hsva(c) => c.to_f32_array_no_alpha(), + Color::Hwba(c) => c.to_f32_array_no_alpha(), + Color::Laba(c) => c.to_f32_array_no_alpha(), + Color::Lcha(c) => c.to_f32_array_no_alpha(), + Color::Oklaba(c) => c.to_f32_array_no_alpha(), + Color::Oklcha(c) => c.to_f32_array_no_alpha(), + Color::Xyza(c) => c.to_f32_array_no_alpha(), + } +} + +#[pymethods] +impl PyColor { + // Varargs ctor for positional calls like color(255, 0, 0). ColorLike handles single-value extraction. + #[new] + #[pyo3(signature = (*args))] + pub fn py_new(args: &Bound<'_, PyTuple>) -> PyResult { + match args.len() { + 0 => Err(PyTypeError::new_err("Color requires at least 1 argument")), + 1 => { + let first = args.get_item(0)?; + if let Ok(c) = first.extract::>() { + return Ok(Self(c.0)); + } + if let Ok(s) = first.extract::() { + return Ok(Self(parse_hex(&s)?)); + } + if let Ok(v) = first.extract::>() { + return Ok(Self(Color::srgba(v.0.x, v.0.y, v.0.z, v.0.w))); + } + let v = extract_component(&first)?; + Ok(Self(Color::srgba(v, v, v, 1.0))) + } + 2 => { + let v = extract_component(&args.get_item(0)?)?; + let a = extract_component(&args.get_item(1)?)?; + Ok(Self(Color::srgba(v, v, v, a))) + } + 3 => { + let r = extract_component(&args.get_item(0)?)?; + let g = extract_component(&args.get_item(1)?)?; + let b = extract_component(&args.get_item(2)?)?; + Ok(Self(Color::srgba(r, g, b, 1.0))) + } + 4 => { + let r = extract_component(&args.get_item(0)?)?; + let g = extract_component(&args.get_item(1)?)?; + let b = extract_component(&args.get_item(2)?)?; + let a = extract_component(&args.get_item(3)?)?; + Ok(Self(Color::srgba(r, g, b, a))) + } + _ => Err(PyTypeError::new_err("Color takes 1-4 arguments")), + } + } + + #[staticmethod] + #[pyo3(signature = (r, g, b, a=1.0))] + pub fn srgb(r: f32, g: f32, b: f32, a: f32) -> Self { + Self(Color::Srgba(Srgba::new(r, g, b, a))) + } + + #[staticmethod] + #[pyo3(signature = (r, g, b, a=1.0))] + pub fn linear(r: f32, g: f32, b: f32, a: f32) -> Self { + Self(Color::LinearRgba(LinearRgba::new(r, g, b, a))) + } + + #[staticmethod] + #[pyo3(signature = (h, s, l, a=1.0))] + pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Self { + Self(Color::Hsla(Hsla::new(h, s, l, a))) + } + + #[staticmethod] + #[pyo3(signature = (h, s, v, a=1.0))] + pub fn hsva(h: f32, s: f32, v: f32, a: f32) -> Self { + Self(Color::Hsva(Hsva::new(h, s, v, a))) + } + + #[staticmethod] + #[pyo3(signature = (h, w, b, a=1.0))] + pub fn hwba(h: f32, w: f32, b: f32, a: f32) -> Self { + Self(Color::Hwba(Hwba::new(h, w, b, a))) + } + + #[staticmethod] + #[pyo3(signature = (l, a_axis, b_axis, alpha=1.0))] + pub fn oklab(l: f32, a_axis: f32, b_axis: f32, alpha: f32) -> Self { + Self(Color::Oklaba(Oklaba::new(l, a_axis, b_axis, alpha))) + } + + #[staticmethod] + #[pyo3(signature = (l, c, h, a=1.0))] + pub fn oklch(l: f32, c: f32, h: f32, a: f32) -> Self { + Self(Color::Oklcha(Oklcha::new(l, c, h, a))) + } + + #[staticmethod] + #[pyo3(signature = (l, a_axis, b_axis, alpha=1.0))] + pub fn lab(l: f32, a_axis: f32, b_axis: f32, alpha: f32) -> Self { + Self(Color::Laba(Laba::new(l, a_axis, b_axis, alpha))) + } + + #[staticmethod] + #[pyo3(signature = (l, c, h, a=1.0))] + pub fn lch(l: f32, c: f32, h: f32, a: f32) -> Self { + Self(Color::Lcha(Lcha::new(l, c, h, a))) + } + + #[staticmethod] + #[pyo3(signature = (x, y, z, a=1.0))] + pub fn xyz(x: f32, y: f32, z: f32, a: f32) -> Self { + Self(Color::Xyza(Xyza::new(x, y, z, a))) + } + + #[staticmethod] + pub fn hex(s: &str) -> PyResult { + parse_hex(s).map(Self) + } + + fn to_srgba(&self) -> Self { + Self(Color::Srgba(self.0.into())) + } + + fn to_linear(&self) -> Self { + Self(Color::LinearRgba(self.0.into())) + } + + fn to_hsla(&self) -> Self { + Self(Color::Hsla(self.0.into())) + } + + fn to_hsva(&self) -> Self { + Self(Color::Hsva(self.0.into())) + } + + fn to_hwba(&self) -> Self { + Self(Color::Hwba(self.0.into())) + } + + fn to_oklab(&self) -> Self { + Self(Color::Oklaba(self.0.into())) + } + + fn to_oklch(&self) -> Self { + Self(Color::Oklcha(self.0.into())) + } + + fn to_lab(&self) -> Self { + Self(Color::Laba(self.0.into())) + } + + fn to_lch(&self) -> Self { + Self(Color::Lcha(self.0.into())) + } + + fn to_xyz(&self) -> Self { + Self(Color::Xyza(self.0.into())) + } + + #[getter] + fn r(&self) -> f32 { + to_srgba(&self.0).red + } + #[setter] + fn set_r(&mut self, val: f32) { + let mut s = to_srgba(&self.0); + s.red = val; + self.0 = Color::Srgba(s); + } + + #[getter] + fn g(&self) -> f32 { + to_srgba(&self.0).green + } + #[setter] + fn set_g(&mut self, val: f32) { + let mut s = to_srgba(&self.0); + s.green = val; + self.0 = Color::Srgba(s); + } + + #[getter] + fn b(&self) -> f32 { + to_srgba(&self.0).blue + } + #[setter] + fn set_b(&mut self, val: f32) { + let mut s = to_srgba(&self.0); + s.blue = val; + self.0 = Color::Srgba(s); + } + + #[getter] + fn a(&self) -> f32 { + self.0.alpha() + } + #[setter] + fn set_a(&mut self, val: f32) { + self.0.set_alpha(val); + } + + fn to_hex(&self) -> String { + to_srgba(&self.0).to_hex() + } + + fn with_alpha(&self, a: f32) -> Self { + Self(self.0.with_alpha(a)) + } + + fn mix(&self, other: &Self, t: f32) -> Self { + Self(self.0.mix(&other.0, t)) + } + + fn lerp(&self, other: &Self, t: f32) -> Self { + self.mix(other, t) + } + + fn lighter(&self, amount: f32) -> Self { + Self(self.0.lighter(amount)) + } + + fn darker(&self, amount: f32) -> Self { + Self(self.0.darker(amount)) + } + + fn luminance(&self) -> f32 { + self.0.luminance() + } + + fn with_luminance(&self, value: f32) -> Self { + Self(self.0.with_luminance(value)) + } + + fn hue(&self) -> f32 { + self.0.hue() + } + + fn with_hue(&self, hue: f32) -> Self { + Self(self.0.with_hue(hue)) + } + + fn rotate_hue(&self, degrees: f32) -> Self { + Self(self.0.rotate_hue(degrees)) + } + + fn saturation(&self) -> f32 { + self.0.saturation() + } + + fn with_saturation(&self, saturation: f32) -> Self { + Self(self.0.with_saturation(saturation)) + } + + fn is_fully_transparent(&self) -> bool { + self.0.is_fully_transparent() + } + + fn is_fully_opaque(&self) -> bool { + self.0.is_fully_opaque() + } + + fn distance(&self, other: &Self) -> f32 { + self.0.distance(&other.0) + } + + fn distance_squared(&self, other: &Self) -> f32 { + self.0.distance_squared(&other.0) + } + + #[staticmethod] + fn gray(lightness: f32) -> Self { + Self(Color::Srgba(Srgba::gray(lightness))) + } + + fn to_vec3(&self) -> crate::math::PyVec3 { + let c = components_no_alpha(&self.0); + crate::math::PyVec3(bevy::math::Vec3::from_array(c)) + } + + fn to_vec4(&self) -> PyVec4 { + let c = components(&self.0); + PyVec4(bevy::math::Vec4::from_array(c)) + } + + fn to_list(&self) -> Vec { + components(&self.0).to_vec() + } + + fn to_tuple<'py>(&self, py: Python<'py>) -> Bound<'py, PyTuple> { + PyTuple::new(py, components(&self.0)).unwrap() + } + + fn __repr__(&self) -> String { + let c = components(&self.0); + format!("Color({}, {}, {}, {})", c[0], c[1], c[2], c[3]) + } + + fn __str__(&self) -> String { + self.__repr__() + } + + fn __eq__(&self, other: &Self) -> bool { + // Compare in sRGBA so colors in different spaces can be equal + to_srgba(&self.0) == to_srgba(&other.0) + } + + fn __hash__(&self) -> u64 { + // Hash in sRGBA so equal colors hash the same regardless of space + let s = to_srgba(&self.0); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + hash_f32(s.red, &mut hasher); + hash_f32(s.green, &mut hasher); + hash_f32(s.blue, &mut hasher); + hash_f32(s.alpha, &mut hasher); + std::hash::Hasher::finish(&hasher) + } + + fn __len__(&self) -> usize { + 4 + } + + fn __getitem__(&self, idx: isize) -> PyResult { + let c = components(&self.0); + let idx = if idx < 0 { 4 + idx } else { idx }; + if idx < 0 || idx >= 4 { + return Err(PyTypeError::new_err("index out of range")); + } + Ok(c[idx as usize]) + } + + fn __iter__(slf: PyRef<'_, Self>) -> PyVecIter { + PyVecIter { + values: components(&slf.0).to_vec(), + index: 0, + } + } +} + +#[derive(FromPyObject)] +pub enum ColorLike { + Instance(PyColor), + HexString(String), + Vec4(PyVec4), + Tuple4((f32, f32, f32, f32)), + Tuple3((f32, f32, f32)), +} + +impl ColorLike { + pub fn into_color(self) -> PyResult { + match self { + ColorLike::Instance(c) => Ok(c.0), + ColorLike::HexString(s) => parse_hex(&s), + ColorLike::Vec4(v) => Ok(Color::srgba(v.0.x, v.0.y, v.0.z, v.0.w)), + ColorLike::Tuple4((r, g, b, a)) => Ok(Color::srgba(r, g, b, a)), + ColorLike::Tuple3((r, g, b)) => Ok(Color::srgba(r, g, b, 1.0)), + } + } +} + +pub(crate) fn extract_color(args: &Bound<'_, PyTuple>) -> PyResult { + PyColor::py_new(args).map(|c| c.0) +} + +fn parse_hex(s: &str) -> PyResult { + Srgba::hex(s) + .map(|srgba| Color::Srgba(srgba)) + .map_err(|e| PyTypeError::new_err(format!("invalid hex color: {e}"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_color_from_srgba() { + let c = PyColor(Color::srgba(1.0, 0.0, 0.5, 1.0)); + let s = to_srgba(&c.0); + assert!((s.red - 1.0).abs() < 1e-6); + assert!((s.green - 0.0).abs() < 1e-6); + assert!((s.blue - 0.5).abs() < 1e-6); + assert!((s.alpha - 1.0).abs() < 1e-6); + } + + #[test] + fn test_hex_roundtrip() { + let c = parse_hex("#FF00FF").unwrap(); + let s = to_srgba(&c); + assert!((s.red - 1.0).abs() < 0.01); + assert!((s.green - 0.0).abs() < 0.01); + assert!((s.blue - 1.0).abs() < 0.01); + assert!((s.alpha - 1.0).abs() < 0.01); + } + + #[test] + fn test_hex_with_alpha() { + let c = parse_hex("#FF000080").unwrap(); + let s = to_srgba(&c); + assert!((s.red - 1.0).abs() < 0.01); + assert!((s.alpha - 128.0 / 255.0).abs() < 0.01); + } + + #[test] + fn test_color_mix() { + let a = PyColor(Color::srgba(0.0, 0.0, 0.0, 1.0)); + let b = PyColor(Color::srgba(1.0, 1.0, 1.0, 1.0)); + let mid = a.mix(&b, 0.5); + let s = to_srgba(&mid.0); + assert!((s.red - 0.5).abs() < 0.05); + assert!((s.green - 0.5).abs() < 0.05); + assert!((s.blue - 0.5).abs() < 0.05); + } + + #[test] + fn test_color_lighter_darker() { + let c = PyColor(Color::srgba(0.5, 0.5, 0.5, 1.0)); + let lighter = c.lighter(0.1); + let darker = c.darker(0.1); + let sl = to_srgba(&lighter.0); + let sd = to_srgba(&darker.0); + assert!(sl.red > sd.red || sl.green > sd.green || sl.blue > sd.blue); + } + + #[test] + fn test_color_with_alpha() { + let c = PyColor(Color::srgba(1.0, 0.0, 0.0, 1.0)); + let transparent = c.with_alpha(0.5); + assert!((transparent.0.alpha() - 0.5).abs() < 1e-6); + } + + #[test] + fn test_color_to_vec4() { + let c = PyColor(Color::srgba(0.25, 0.5, 0.75, 1.0)); + let v = c.to_vec4(); + assert!((v.0.x - 0.25).abs() < 1e-6); + assert!((v.0.y - 0.5).abs() < 1e-6); + assert!((v.0.z - 0.75).abs() < 1e-6); + assert!((v.0.w - 1.0).abs() < 1e-6); + } + + #[test] + fn test_color_eq() { + let a = PyColor(Color::srgba(1.0, 0.0, 0.0, 1.0)); + let b = PyColor(Color::srgba(1.0, 0.0, 0.0, 1.0)); + assert!(a.__eq__(&b)); + } + + #[test] + fn test_hsla_roundtrip() { + let c = PyColor::hsla(0.0, 1.0, 0.5, 1.0); + let s = to_srgba(&c.0); + assert!((s.red - 1.0).abs() < 0.01); + assert!(s.green < 0.01); + assert!(s.blue < 0.01); + + let [h, sat, l, a] = components(&c.to_hsla().0); + assert!((h - 0.0).abs() < 0.5); + assert!((sat - 1.0).abs() < 0.01); + assert!((l - 0.5).abs() < 0.01); + assert!((a - 1.0).abs() < 0.01); + } + + #[test] + fn test_oklch_roundtrip() { + let c = PyColor::oklch(0.7, 0.15, 30.0, 1.0); + let [l, ch, h, a] = components(&c.to_oklch().0); + assert!((l - 0.7).abs() < 0.01); + assert!((ch - 0.15).abs() < 0.01); + assert!((h - 30.0).abs() < 0.5); + assert!((a - 1.0).abs() < 0.01); + } + + #[test] + fn test_linear_roundtrip() { + let c = PyColor::linear(0.5, 0.25, 0.1, 0.8); + let [r, g, b, a] = components(&c.to_linear().0); + assert!((r - 0.5).abs() < 0.01); + assert!((g - 0.25).abs() < 0.01); + assert!((b - 0.1).abs() < 0.01); + assert!((a - 0.8).abs() < 0.01); + } + + #[test] + fn test_to_list() { + let c = PyColor(Color::srgba(0.1, 0.2, 0.3, 0.4)); + let list = c.to_list(); + assert_eq!(list.len(), 4); + assert!((list[0] - 0.1).abs() < 1e-6); + assert!((list[3] - 0.4).abs() < 1e-6); + } +} diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index d8d8929..152e03b 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -299,9 +299,9 @@ impl Graphics { } } - pub fn background(&self, args: Vec) -> PyResult<()> { - let (r, g, b, a) = parse_color(&args)?; - let color = bevy::color::Color::srgba(r, g, b, a); + #[pyo3(signature = (*args))] + pub fn background(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + let color = crate::color::extract_color(args)?; graphics_record_command(self.entity, DrawCommand::BackgroundColor(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -311,9 +311,9 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn fill(&self, args: Vec) -> PyResult<()> { - let (r, g, b, a) = parse_color(&args)?; - let color = bevy::color::Color::srgba(r, g, b, a); + #[pyo3(signature = (*args))] + pub fn fill(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + let color = crate::color::extract_color(args)?; graphics_record_command(self.entity, DrawCommand::Fill(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -323,9 +323,9 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn stroke(&self, args: Vec) -> PyResult<()> { - let (r, g, b, a) = parse_color(&args)?; - let color = bevy::color::Color::srgba(r, g, b, a); + #[pyo3(signature = (*args))] + pub fn stroke(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + let color = crate::color::extract_color(args)?; graphics_record_command(self.entity, DrawCommand::StrokeColor(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -448,9 +448,9 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn emissive(&self, args: Vec) -> PyResult<()> { - let (r, g, b, a) = parse_color(&args)?; - let color = bevy::color::Color::srgba(r, g, b, a); + #[pyo3(signature = (*args))] + pub fn emissive(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + let color = crate::color::extract_color(args)?; graphics_record_command(self.entity, DrawCommand::Emissive(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -550,8 +550,12 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn light_directional(&self, r: f32, g: f32, b: f32, illuminance: f32) -> PyResult { - let color = bevy::color::Color::srgb(r, g, b); + pub fn light_directional( + &self, + color: crate::color::ColorLike, + illuminance: f32, + ) -> PyResult { + let color = color.into_color()?; match light_create_directional(self.entity, color, illuminance) { Ok(light) => Ok(Light { entity: light }), Err(e) => Err(PyRuntimeError::new_err(format!("{e}"))), @@ -560,14 +564,12 @@ impl Graphics { pub fn light_point( &self, - r: f32, - g: f32, - b: f32, + color: crate::color::ColorLike, intensity: f32, range: f32, radius: f32, ) -> PyResult { - let color = bevy::color::Color::srgb(r, g, b); + let color = color.into_color()?; match light_create_point(self.entity, color, intensity, range, radius) { Ok(light) => Ok(Light { entity: light }), Err(e) => Err(PyRuntimeError::new_err(format!("{e}"))), @@ -576,16 +578,14 @@ impl Graphics { pub fn light_spot( &self, - r: f32, - g: f32, - b: f32, + color: crate::color::ColorLike, intensity: f32, range: f32, radius: f32, inner_angle: f32, outer_angle: f32, ) -> PyResult { - let color = bevy::color::Color::srgb(r, g, b); + let color = color.into_color()?; match light_create_spot( self.entity, color, @@ -601,29 +601,6 @@ impl Graphics { } } -// TODO: a real color type. or color parser? idk. color is confusing. let's think -// about how to expose different color spaces in an idiomatic pythonic way -fn parse_color(args: &[f32]) -> PyResult<(f32, f32, f32, f32)> { - match args.len() { - 1 => { - let v = args[0] / 255.0; - Ok((v, v, v, 1.0)) - } - 2 => { - let v = args[0] / 255.0; - Ok((v, v, v, args[1] / 255.0)) - } - 3 => Ok((args[0] / 255.0, args[1] / 255.0, args[2] / 255.0, 1.0)), - 4 => Ok(( - args[0] / 255.0, - args[1] / 255.0, - args[2] / 255.0, - args[3] / 255.0, - )), - _ => Err(PyRuntimeError::new_err("color requires 1-4 arguments")), - } -} - pub fn get_graphics<'py>(module: &Bound<'py, PyModule>) -> PyResult>> { let Ok(attr) = module.getattr("_graphics") else { return Ok(None); diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 29a0e10..b127b9f 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -8,6 +8,7 @@ //! //! To allow Python users to create a similar experience, we provide module-level //! functions that forward to a singleton Graphics object pub(crate) behind the scenes. +pub(crate) mod color; mod glfw; mod gltf; mod graphics; @@ -157,26 +158,105 @@ mod mewnala { #[pyfunction] #[pyo3(signature = (*args))] - fn vec2(args: &Bound<'_, PyTuple>) -> PyResult { - crate::math::PyVec2::py_new(args) + fn vec2(args: &Bound<'_, PyTuple>) -> PyResult { + PyVec2::py_new(args) } #[pyfunction] #[pyo3(signature = (*args))] - fn vec3(args: &Bound<'_, PyTuple>) -> PyResult { - crate::math::PyVec3::py_new(args) + fn vec3(args: &Bound<'_, PyTuple>) -> PyResult { + PyVec3::py_new(args) } #[pyfunction] #[pyo3(signature = (*args))] - fn vec4(args: &Bound<'_, PyTuple>) -> PyResult { - crate::math::PyVec4::py_new(args) + fn vec4(args: &Bound<'_, PyTuple>) -> PyResult { + PyVec4::py_new(args) } #[pyfunction] #[pyo3(signature = (*args))] - fn quat(args: &Bound<'_, PyTuple>) -> PyResult { - crate::math::PyQuat::py_new(args) + fn quat(args: &Bound<'_, PyTuple>) -> PyResult { + PyQuat::py_new(args) + } + } + + #[pymodule] + mod color { + use super::*; + + #[pymodule_export] + use crate::color::PyColor; + + #[pyfunction] + #[pyo3(signature = (*args))] + fn color(args: &Bound<'_, PyTuple>) -> PyResult { + PyColor::py_new(args) + } + + #[pyfunction] + fn hex(s: &str) -> PyResult { + PyColor::hex(s) + } + + #[pyfunction] + #[pyo3(signature = (r, g, b, a=1.0))] + fn srgb(r: f32, g: f32, b: f32, a: f32) -> PyColor { + PyColor::srgb(r, g, b, a) + } + + #[pyfunction] + #[pyo3(signature = (r, g, b, a=1.0))] + fn linear(r: f32, g: f32, b: f32, a: f32) -> PyColor { + PyColor::linear(r, g, b, a) + } + + #[pyfunction] + #[pyo3(signature = (h, s, l, a=1.0))] + fn hsla(h: f32, s: f32, l: f32, a: f32) -> PyColor { + PyColor::hsla(h, s, l, a) + } + + #[pyfunction] + #[pyo3(signature = (h, s, v, a=1.0))] + fn hsva(h: f32, s: f32, v: f32, a: f32) -> PyColor { + PyColor::hsva(h, s, v, a) + } + + #[pyfunction] + #[pyo3(signature = (h, w, b, a=1.0))] + fn hwba(h: f32, w: f32, b: f32, a: f32) -> PyColor { + PyColor::hwba(h, w, b, a) + } + + #[pyfunction] + #[pyo3(signature = (l, a_axis, b_axis, alpha=1.0))] + fn oklab(l: f32, a_axis: f32, b_axis: f32, alpha: f32) -> PyColor { + PyColor::oklab(l, a_axis, b_axis, alpha) + } + + #[pyfunction] + #[pyo3(signature = (l, c, h, a=1.0))] + fn oklch(l: f32, c: f32, h: f32, a: f32) -> PyColor { + PyColor::oklch(l, c, h, a) + } + + #[pyfunction] + #[pyo3(signature = (l, a_axis, b_axis, alpha=1.0))] + fn lab(l: f32, a_axis: f32, b_axis: f32, alpha: f32) -> PyColor { + PyColor::lab(l, a_axis, b_axis, alpha) + } + + #[pyfunction] + #[pyo3(signature = (l, c, h, a=1.0))] + fn lch(l: f32, c: f32, h: f32, a: f32) -> PyColor { + PyColor::lch(l, c, h, a) + } + + #[pyfunction] + #[pyo3(signature = (x, y, z, a=1.0))] + fn xyz(x: f32, y: f32, z: f32, a: f32) -> PyColor { + PyColor::xyz(x, y, z, a) } } @@ -442,13 +522,13 @@ mod mewnala { if first.is_instance_of::() { graphics.background_image(&*first.extract::>()?) } else { - graphics.background(args.extract()?) + graphics.background(args) } } #[pyfunction] #[pyo3(pass_module, signature = (*args))] - fn fill(module: &Bound<'_, PyModule>, args: Vec) -> PyResult<()> { + fn fill(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { graphics!(module).fill(args) } @@ -460,7 +540,7 @@ mod mewnala { #[pyfunction] #[pyo3(pass_module, signature = (*args))] - fn stroke(module: &Bound<'_, PyModule>, args: Vec) -> PyResult<()> { + fn stroke(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { graphics!(module).stroke(args) } @@ -513,42 +593,36 @@ mod mewnala { } #[pyfunction] - #[pyo3(pass_module, signature = (r, g, b, illuminance))] + #[pyo3(pass_module)] fn create_directional_light( module: &Bound<'_, PyModule>, - r: f32, - g: f32, - b: f32, + color: super::color::ColorLike, illuminance: f32, ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_directional(r, g, b, illuminance) + graphics.light_directional(color, illuminance) } #[pyfunction] - #[pyo3(pass_module, signature = (r, g, b, intensity, range, radius))] + #[pyo3(pass_module)] fn create_point_light( module: &Bound<'_, PyModule>, - r: f32, - g: f32, - b: f32, + color: super::color::ColorLike, intensity: f32, range: f32, radius: f32, ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_point(r, g, b, intensity, range, radius) + graphics.light_point(color, intensity, range, radius) } #[pyfunction] - #[pyo3(pass_module, signature = (r, g, b, intensity, range, radius, inner_angle, outer_angle))] + #[pyo3(pass_module)] fn create_spot_light( module: &Bound<'_, PyModule>, - r: f32, - g: f32, - b: f32, + color: super::color::ColorLike, intensity: f32, range: f32, radius: f32, @@ -557,7 +631,7 @@ mod mewnala { ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_spot(r, g, b, intensity, range, radius, inner_angle, outer_angle) + graphics.light_spot(color, intensity, range, radius, inner_angle, outer_angle) } #[pyfunction(name = "sphere")] @@ -592,7 +666,6 @@ mod mewnala { #[pyfunction] #[pyo3(pass_module, signature = (*args))] fn emissive(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let args: Vec = args.extract()?; graphics!(module).emissive(args) } diff --git a/crates/processing_pyo3/src/math.rs b/crates/processing_pyo3/src/math.rs index 0239e01..e923dee 100644 --- a/crates/processing_pyo3/src/math.rs +++ b/crates/processing_pyo3/src/math.rs @@ -3,7 +3,7 @@ use std::hash::{Hash, Hasher}; use bevy::math::{EulerRot, Quat, Vec2, Vec3, Vec4}; use pyo3::{exceptions::PyTypeError, prelude::*, types::PyTuple}; -fn hash_f32(val: f32, state: &mut impl Hasher) { +pub fn hash_f32(val: f32, state: &mut impl Hasher) { if val == 0.0 { 0.0f32.to_bits().hash(state); } else { @@ -519,8 +519,8 @@ impl PyQuat { #[pyclass] pub struct PyVecIter { - values: Vec, - index: usize, + pub(crate) values: Vec, + pub(crate) index: usize, } #[pymethods]