From 9f9152f9694208d386728769e38fd9c15616ab46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 2 May 2026 14:25:58 -0700 Subject: [PATCH 1/6] Refactor light helpers to use correct naming. --- .../examples/camera_controllers.py | 2 +- crates/processing_pyo3/examples/lights.py | 8 ++-- crates/processing_pyo3/examples/materials.py | 4 +- crates/processing_pyo3/src/lib.rs | 46 +++++++++++++++---- 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/crates/processing_pyo3/examples/camera_controllers.py b/crates/processing_pyo3/examples/camera_controllers.py index 7feae0f..562d5b6 100644 --- a/crates/processing_pyo3/examples/camera_controllers.py +++ b/crates/processing_pyo3/examples/camera_controllers.py @@ -8,7 +8,7 @@ def setup(): mode_3d() orbit_camera() - dir_light = create_directional_light((1.0, 0.98, 0.95), 1500.0) + dir_light = directional_light((1.0, 0.98, 0.95), 1500.0) dir_light.position(300.0, 400.0, 300.0) dir_light.look_at(0.0, 0.0, 0.0) diff --git a/crates/processing_pyo3/examples/lights.py b/crates/processing_pyo3/examples/lights.py index e825893..66552a9 100644 --- a/crates/processing_pyo3/examples/lights.py +++ b/crates/processing_pyo3/examples/lights.py @@ -7,19 +7,19 @@ def setup(): mode_3d() # Directional Light - dir_light = create_directional_light((0.5, 0.24, 1.0), 1500.0) + dir_light = directional_light((0.5, 0.24, 1.0), 1500.0) # Point Lights - point_light_a = create_point_light((1.0, 0.5, 0.25), 1000000.0, 200.0, 0.5) + point_light_a = point_light((1.0, 0.5, 0.25), 1000000.0, 200.0, 0.5) point_light_a.position(-25.0, 5.0, 51.0) point_light_a.look_at(0.0, 0.0, 0.0) - point_light_b = create_point_light((0.0, 0.5, 0.75), 2000000.0, 200.0, 0.25) + point_light_b = point_light((0.0, 0.5, 0.75), 2000000.0, 200.0, 0.25) point_light_b.position(0.0, 5.0, 50.5) point_light_b.look_at(0.0, 0.0, 0.0) # Spot Light - spot_light = create_spot_light((0.25, 0.8, 0.19), 15.0 * 1000000.0, 200.0, 0.84, 0.0, 0.7854) + spot_light = spot_light((0.25, 0.8, 0.19), 15.0 * 1000000.0, 200.0, 0.84, 0.0, 0.7854) spot_light.position(40.0, 0.0, 70.0) spot_light.look_at(0.0, 0.0, 0.0) diff --git a/crates/processing_pyo3/examples/materials.py b/crates/processing_pyo3/examples/materials.py index ffa66cc..2767e8a 100644 --- a/crates/processing_pyo3/examples/materials.py +++ b/crates/processing_pyo3/examples/materials.py @@ -7,8 +7,8 @@ def setup(): size(800, 600) mode_3d() - dir_light = create_directional_light((1.0, 0.98, 0.95), 1500.0) - point_light = create_point_light((1.0, 1.0, 1.0), 100000.0, 800.0, 0.0) + dir_light = directional_light((1.0, 0.98, 0.95), 1500.0) + point_light = point_light((1.0, 1.0, 1.0), 100000.0, 800.0, 0.0) point_light.position(200.0, 200.0, 400.0) mat = Material() diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 7902979..66b01a3 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1370,35 +1370,59 @@ mod mewnala { graphics.create_image(width, height) } + fn apply_light_transform( + light: &Light, + position: Option, + look_at: Option, + ) -> PyResult<()> { + if let Some(p) = position { + ::processing::prelude::transform_set_position(light.entity, p.into_vec3()) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + } + if let Some(la) = look_at { + ::processing::prelude::transform_look_at(light.entity, la.into_vec3()) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + } + Ok(()) + } + #[pyfunction] - #[pyo3(pass_module)] - fn create_directional_light( + #[pyo3(pass_module, signature = (color, illuminance, *, position=None, look_at=None))] + fn directional_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, illuminance: f32, + position: Option, + look_at: Option, ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_directional(color, illuminance) + let light = graphics.light_directional(color, illuminance)?; + apply_light_transform(&light, position, look_at)?; + Ok(light) } #[pyfunction] - #[pyo3(pass_module)] - fn create_point_light( + #[pyo3(pass_module, signature = (color, intensity, range, radius, *, position=None, look_at=None))] + fn point_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, intensity: f32, range: f32, radius: f32, + position: Option, + look_at: Option, ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_point(color, intensity, range, radius) + let light = graphics.light_point(color, intensity, range, radius)?; + apply_light_transform(&light, position, look_at)?; + Ok(light) } #[pyfunction] - #[pyo3(pass_module)] - fn create_spot_light( + #[pyo3(pass_module, signature = (color, intensity, range, radius, inner_angle, outer_angle, *, position=None, look_at=None))] + fn spot_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, intensity: f32, @@ -1406,10 +1430,14 @@ mod mewnala { radius: f32, inner_angle: f32, outer_angle: f32, + position: Option, + look_at: Option, ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - graphics.light_spot(color, intensity, range, radius, inner_angle, outer_angle) + let light = graphics.light_spot(color, intensity, range, radius, inner_angle, outer_angle)?; + apply_light_transform(&light, position, look_at)?; + Ok(light) } #[pyfunction(name = "sphere")] From 2766a4f886834ef6d3474a8ffda898071f5bd047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 2 May 2026 14:35:16 -0700 Subject: [PATCH 2/6] Fix color conflict. --- crates/processing_pyo3/src/lib.rs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 66b01a3..26acf9b 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -666,6 +666,19 @@ mod mewnala { #[pymodule_export] use crate::color::PyColor; + #[pyfunction(name = "color")] + #[pyo3(signature = (*args))] + fn color_ctor(py: Python<'_>, args: &Bound<'_, PyTuple>) -> PyResult { + let parent = py.import("mewnala.mewnala")?; + match get_graphics(&parent)? { + Some(g) => g.color(args), + None => { + let mode = crate::color::ColorMode::default(); + crate::color::extract_color_with_mode(args, &mode).map(PyColor::from) + } + } + } + #[pyfunction] fn hex(s: &str) -> PyResult { PyColor::hex(s) @@ -1248,21 +1261,6 @@ mod mewnala { graphics!(module).draw_geometry(&*geometry.extract::>()?) } - #[pyfunction(name = "color")] - #[pyo3(pass_module, signature = (*args))] - fn create_color( - module: &Bound<'_, PyModule>, - args: &Bound<'_, PyTuple>, - ) -> PyResult { - match get_graphics(module)? { - Some(g) => g.color(args), - None => { - let mode = super::color::ColorMode::default(); - super::color::extract_color_with_mode(args, &mode).map(super::color::PyColor::from) - } - } - } - #[pyfunction] #[pyo3(pass_module, signature = (*args))] fn background(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { From 28e08f65d246bae836971019f1273ca28c2180db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 2 May 2026 14:35:31 -0700 Subject: [PATCH 3/6] Add vector constants. --- crates/processing_pyo3/src/math.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/processing_pyo3/src/math.rs b/crates/processing_pyo3/src/math.rs index 2abde01..5e19d9b 100644 --- a/crates/processing_pyo3/src/math.rs +++ b/crates/processing_pyo3/src/math.rs @@ -504,6 +504,11 @@ macro_rules! impl_py_vec { } impl_py_vec!(PyVec2, "Vec2", 2, [(x, set_x, 0), (y, set_y, 1)], Vec2, extra { + #[classattr] #[allow(non_snake_case)] fn ZERO() -> Self { Self(Vec2::ZERO) } + #[classattr] #[allow(non_snake_case)] fn ONE() -> Self { Self(Vec2::ONE) } + #[classattr] #[allow(non_snake_case)] fn X() -> Self { Self(Vec2::X) } + #[classattr] #[allow(non_snake_case)] fn Y() -> Self { Self(Vec2::Y) } + fn angle(&self) -> f32 { self.0.y.atan2(self.0.x) } @@ -543,6 +548,12 @@ impl_py_vec!(PyVec2, "Vec2", 2, [(x, set_x, 0), (y, set_y, 1)], Vec2, extra { }); impl_py_vec!(PyVec3, "Vec3", 3, [(x, set_x, 0), (y, set_y, 1), (z, set_z, 2)], Vec3, extra { + #[classattr] #[allow(non_snake_case)] fn ZERO() -> Self { Self(Vec3::ZERO) } + #[classattr] #[allow(non_snake_case)] fn ONE() -> Self { Self(Vec3::ONE) } + #[classattr] #[allow(non_snake_case)] fn X() -> Self { Self(Vec3::X) } + #[classattr] #[allow(non_snake_case)] fn Y() -> Self { Self(Vec3::Y) } + #[classattr] #[allow(non_snake_case)] fn Z() -> Self { Self(Vec3::Z) } + fn cross(&self, other: &Self) -> Self { Self(self.0.cross(other.0)) } @@ -570,6 +581,13 @@ impl_py_vec!( [(x, set_x, 0), (y, set_y, 1), (z, set_z, 2), (w, set_w, 3)], Vec4, extra { + #[classattr] #[allow(non_snake_case)] fn ZERO() -> Self { Self(Vec4::ZERO) } + #[classattr] #[allow(non_snake_case)] fn ONE() -> Self { Self(Vec4::ONE) } + #[classattr] #[allow(non_snake_case)] fn X() -> Self { Self(Vec4::X) } + #[classattr] #[allow(non_snake_case)] fn Y() -> Self { Self(Vec4::Y) } + #[classattr] #[allow(non_snake_case)] fn Z() -> Self { Self(Vec4::Z) } + #[classattr] #[allow(non_snake_case)] fn W() -> Self { Self(Vec4::W) } + fn truncate(&self) -> PyVec3 { PyVec3(self.0.truncate()) } From 8f3bf0fede79e300f5c9e3aa45a7e6ea0d0c5105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 2 May 2026 14:45:31 -0700 Subject: [PATCH 4/6] Fix AppExit panic. --- crates/processing_core/src/lib.rs | 15 +++++++++++---- crates/processing_pyo3/src/lib.rs | 10 ++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/processing_core/src/lib.rs b/crates/processing_core/src/lib.rs index 9de4d39..b15688b 100644 --- a/crates/processing_core/src/lib.rs +++ b/crates/processing_core/src/lib.rs @@ -14,14 +14,21 @@ thread_local! { } pub fn app_mut(cb: impl FnOnce(&mut App) -> error::Result) -> error::Result { - let res = APP.with(|app_cell| { - let mut app_borrow = app_cell.borrow_mut(); + // `try_with` so a `Drop` running after the TLS is destroyed sees an + // `AppAccess` error rather than panicking + let res = APP.try_with(|app_cell| { + let mut app_borrow = app_cell + .try_borrow_mut() + .map_err(|_| error::ProcessingError::AppAccess)?; let app = app_borrow .as_mut() .ok_or(error::ProcessingError::AppAccess)?; cb(app) - })?; - Ok(res) + }); + match res { + Ok(inner) => inner, + Err(_) => Err(error::ProcessingError::AppAccess), + } } pub fn is_already_init() -> error::Result { diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 26acf9b..701d7ec 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1017,7 +1017,7 @@ mod mewnala { return Ok(()); } - Python::attach(|py| { + let result: PyResult<()> = Python::attach(|py| { let builtins = PyModule::import(py, "builtins")?; let locals = builtins.getattr("locals")?.call0()?; @@ -1138,7 +1138,13 @@ mod mewnala { } Ok(()) - }) + }); + + // tear the app down here while the TLS is still alive; the eager + // TLS destructor aborts inside a Bevy resource drop + let _ = ::processing::exit(0); + + result } #[pyfunction] From 24d4ca2c9e0a8019da0d159380f604f047cf4611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 2 May 2026 14:45:50 -0700 Subject: [PATCH 5/6] Fix near plane. --- crates/processing_render/src/graphics.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index f885750..001f9f5 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -303,11 +303,12 @@ pub fn mode_3d( let fov = std::f32::consts::PI / 3.0; // 60 degrees let aspect = width / height; let camera_z = (height / 2.0) / (fov / 2.0).tan(); - let near = camera_z / 10.0; + // Processing4 uses near = camera_z / 10, but that clips anything closer + // than ~camera_z/10 to the camera. Since `transform_set_position` lets the + // user move the camera without recomputing the projection, a small fixed + // near is safer and matches most engines' defaults. + let near = 1.0; let far = camera_z * 10.0; - - // TODO: Setting this as a default, but we need to think about API around - // a user defined value let near_clip_plane = vec4(0.0, 0.0, -1.0, -near); let mut projection = projections From 16e573b58fdadb2ef93e4118537bfa86fe61d07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 2 May 2026 14:59:32 -0700 Subject: [PATCH 6/6] Fmt. --- crates/processing_pyo3/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 701d7ec..c32d62e 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1439,7 +1439,8 @@ mod mewnala { ) -> PyResult { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - let light = graphics.light_spot(color, intensity, range, radius, inner_angle, outer_angle)?; + let light = + graphics.light_spot(color, intensity, range, radius, inner_angle, outer_angle)?; apply_light_transform(&light, position, look_at)?; Ok(light) }