diff --git a/Cargo.toml b/Cargo.toml index 9e64703..dec299e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ workspace = true default = ["wayland"] wayland = ["processing_render/wayland"] x11 = ["processing_render/x11"] +python = ["processing_render/python"] [workspace] resolver = "3" diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 8234147..8bacbe5 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -17,7 +17,7 @@ mod error; #[unsafe(no_mangle)] pub extern "C" fn processing_init() { error::clear_error(); - error::check(init); + error::check(|| init(None)); } /// Create a WebGPU surface from a macOS NSWindow handle. diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index 4e0acb7..5681d37 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -17,7 +17,7 @@ x11 = ["processing/x11"] [dependencies] pyo3 = "0.27.0" -processing = { workspace = true } +processing = { workspace = true, features = ["python"] } bevy = { workspace = true } glfw = { version = "0.60.0"} diff --git a/crates/processing_pyo3/examples/assets/images/logo.png b/crates/processing_pyo3/examples/assets/images/logo.png new file mode 100644 index 0000000..cf0b2e0 Binary files /dev/null and b/crates/processing_pyo3/examples/assets/images/logo.png differ diff --git a/crates/processing_pyo3/examples/background_image.py b/crates/processing_pyo3/examples/background_image.py new file mode 100644 index 0000000..632dcd9 --- /dev/null +++ b/crates/processing_pyo3/examples/background_image.py @@ -0,0 +1,12 @@ +from processing import * + +def setup(): + size(800, 600) + +def draw(): + background(220) + image("images/logo.png") + + +# TODO: this should happen implicitly on module load somehow +run() diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index f777edc..0e7de29 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -38,11 +38,13 @@ impl Drop for Graphics { #[pymethods] impl Graphics { #[new] - pub fn new(width: u32, height: u32) -> PyResult { + pub fn new(width: u32, height: u32, asset_path: &str) -> PyResult { let glfw_ctx = GlfwContext::new(width, height).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - init().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + let mut config = Config::new(); + config.set(ConfigKey::AssetRootPath, asset_path.to_string()); + init(Some(config)).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; let surface = glfw_ctx .create_surface(width, height, 1.0) @@ -122,6 +124,12 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + pub fn image(&self, file: &str) -> PyResult<()> { + let image = image_load(file).unwrap(); + graphics_record_command(self.entity, DrawCommand::BackgroundImage(image)) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn push_matrix(&self) -> PyResult<()> { graphics_record_command(self.entity, DrawCommand::PushMatrix) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 432c433..258573e 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -7,7 +7,7 @@ //! receiver. //! //! To allow Python users to create a similar experience, we provide module-level -//! functions that forward to a singleton Graphics object bepub(crate) pub(crate) hind the scenes. +//! functions that forward to a singleton Graphics object pub(crate) behind the scenes. mod glfw; mod graphics; @@ -16,23 +16,37 @@ use pyo3::{exceptions::PyRuntimeError, prelude::*}; #[pymodule] fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_function(wrap_pyfunction!(size, m)?)?; - m.add_function(wrap_pyfunction!(run, m)?)?; - m.add_function(wrap_pyfunction!(background, m)?)?; - m.add_function(wrap_pyfunction!(fill, m)?)?; - m.add_function(wrap_pyfunction!(no_fill, m)?)?; - m.add_function(wrap_pyfunction!(stroke, m)?)?; - m.add_function(wrap_pyfunction!(no_stroke, m)?)?; - m.add_function(wrap_pyfunction!(stroke_weight, m)?)?; - m.add_function(wrap_pyfunction!(rect, m)?)?; - Ok(()) + Python::attach(|py| { + let sys = PyModule::import(py, "sys")?; + let argv: Vec = sys.getattr("argv")?.extract()?; + let filename: &str = argv[0].as_str(); + let os = PyModule::import(py, "os")?; + let path = os.getattr("path")?; + let dirname = path.getattr("dirname")?.call1((filename,))?; + let abspath = path.getattr("abspath")?.call1((dirname,))?; + let abspath = path.getattr("join")?.call1((abspath, "assets"))?; + + m.add("_root_dir", abspath)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(size, m)?)?; + m.add_function(wrap_pyfunction!(run, m)?)?; + m.add_function(wrap_pyfunction!(background, m)?)?; + m.add_function(wrap_pyfunction!(fill, m)?)?; + m.add_function(wrap_pyfunction!(no_fill, m)?)?; + m.add_function(wrap_pyfunction!(stroke, m)?)?; + m.add_function(wrap_pyfunction!(no_stroke, m)?)?; + m.add_function(wrap_pyfunction!(stroke_weight, m)?)?; + m.add_function(wrap_pyfunction!(rect, m)?)?; + m.add_function(wrap_pyfunction!(image, m)?)?; + Ok(()) + }) } #[pyfunction] #[pyo3(pass_module)] fn size(module: &Bound<'_, PyModule>, width: u32, height: u32) -> PyResult<()> { - let graphics = Graphics::new(width, height)?; + let asset_path: String = module.getattr("_root_dir")?.extract()?; + let graphics = Graphics::new(width, height, asset_path.as_str())?; module.setattr("_graphics", graphics)?; Ok(()) } @@ -122,3 +136,9 @@ fn rect( ) -> PyResult<()> { get_graphics(module)?.rect(x, y, w, h, tl, tr, br, bl) } + +#[pyfunction] +#[pyo3(pass_module, signature = (image_file))] +fn image(module: &Bound<'_, PyModule>, image_file: &str) -> PyResult<()> { + get_graphics(module)?.image(image_file) +} diff --git a/crates/processing_render/Cargo.toml b/crates/processing_render/Cargo.toml index 72272e3..1cae470 100644 --- a/crates/processing_render/Cargo.toml +++ b/crates/processing_render/Cargo.toml @@ -10,6 +10,7 @@ workspace = true default = [] wayland = ["bevy/wayland"] x11 = ["bevy/x11"] +python = [] [dependencies] bevy = { workspace = true } @@ -32,4 +33,4 @@ windows = { version = "0.58", features = ["Win32_Foundation", "Win32_System_Libr wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" js-sys = "0.3" -web-sys = { version = "0.3", features = ["Window", "Document", "HtmlCanvasElement"] } \ No newline at end of file +web-sys = { version = "0.3", features = ["Window", "Document", "HtmlCanvasElement"] } diff --git a/crates/processing_render/src/config.rs b/crates/processing_render/src/config.rs new file mode 100644 index 0000000..a09f0fb --- /dev/null +++ b/crates/processing_render/src/config.rs @@ -0,0 +1,32 @@ +//! Options object for configuring various aspects of libprocessing. +//! +//! To add a new Config just add a new enum with associated value + +use std::collections::HashMap; + +#[derive(Hash, Eq, PartialEq)] +pub enum ConfigKey { + AssetRootPath, +} +// TODO: Consider Box instead of String +pub type ConfigMap = HashMap; +pub struct Config { + map: ConfigMap, +} + +impl Config { + pub fn new() -> Self { + // TODO consider defaults + Config { + map: ConfigMap::new(), + } + } + + pub fn get(&self, k: ConfigKey) -> Option<&String> { + self.map.get(&k) + } + + pub fn set(&mut self, k: ConfigKey, v: String) { + self.map.insert(k, v); + } +} diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index b038087..e675b08 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -3,6 +3,8 @@ //! It can be created from raw pixel data, loaded from disk, resized, and read back to CPU memory. use std::path::PathBuf; +#[cfg(feature = "python")] +use bevy::asset::{AssetPath, io::AssetSourceId}; use bevy::{ asset::{ LoadState, RenderAssetUsages, handle_internal_asset_events, io::embedded::GetAssetServer, @@ -138,6 +140,9 @@ pub fn from_handle( } pub fn load(In(path): In, world: &mut World) -> Result { + #[cfg(feature = "python")] + let path = AssetPath::from_path_buf(path).with_source(AssetSourceId::from("assets_directory")); + let handle: Handle = world.get_asset_server().load(path); while let LoadState::Loading = world.get_asset_server().load_state(&handle) { world.run_system_once(handle_internal_asset_events).unwrap(); diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 2beef68..e862292 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod error; mod graphics; pub mod image; @@ -6,6 +7,10 @@ mod surface; use std::{cell::RefCell, num::NonZero, path::PathBuf, sync::OnceLock}; +use config::*; + +#[cfg(feature = "python")] +use bevy::asset::io::AssetSourceBuilder; #[cfg(not(target_arch = "wasm32"))] use bevy::log::tracing_subscriber; use bevy::{ @@ -202,7 +207,7 @@ pub fn surface_resize(graphics_entity: Entity, width: u32, height: u32) -> error }) } -fn create_app() -> App { +fn create_app(_config: Config) -> App { let mut app = App::new(); #[cfg(not(target_arch = "wasm32"))] @@ -227,6 +232,15 @@ fn create_app() -> App { ..default() }); + #[cfg(feature = "python")] + { + let asset_path = _config.get(ConfigKey::AssetRootPath).unwrap(); + app.register_asset_source( + "assets_directory", + AssetSourceBuilder::platform_default(asset_path, None), + ); + } + app.add_plugins(plugins); app.add_plugins((ImagePlugin, GraphicsPlugin, SurfacePlugin)); app.add_systems(First, (clear_transient_meshes, activate_cameras)) @@ -257,14 +271,17 @@ fn set_app(app: App) { /// Initialize the app, if not already initialized. Must be called from the main thread and cannot /// be called concurrently from multiple threads. +/// asset_path is Optional because only python needs to use it. #[cfg(not(target_arch = "wasm32"))] -pub fn init() -> error::Result<()> { +pub fn init(config: Option) -> error::Result<()> { + let config = config.unwrap_or_else(|| Config::new()); + setup_tracing()?; if is_already_init()? { return Ok(()); } - let mut app = create_app(); + let mut app = create_app(config); app.finish(); app.cleanup(); set_app(app); diff --git a/examples/background_image.rs b/examples/background_image.rs index 2f98eb6..0bfdfca 100644 --- a/examples/background_image.rs +++ b/examples/background_image.rs @@ -19,7 +19,7 @@ fn main() { fn sketch() -> error::Result<()> { let mut glfw_ctx = GlfwContext::new(400, 400)?; - init()?; + init(None)?; let width = 400; let height = 400; diff --git a/examples/rectangle.rs b/examples/rectangle.rs index 9e372e1..66ddff5 100644 --- a/examples/rectangle.rs +++ b/examples/rectangle.rs @@ -19,7 +19,7 @@ fn main() { fn sketch() -> error::Result<()> { let mut glfw_ctx = GlfwContext::new(400, 400)?; - init()?; + init(None)?; let width = 400; let height = 400; diff --git a/examples/transforms.rs b/examples/transforms.rs index 2cecaa8..3ac1c5b 100644 --- a/examples/transforms.rs +++ b/examples/transforms.rs @@ -13,7 +13,7 @@ fn main() { fn sketch() -> error::Result<()> { let mut glfw_ctx = GlfwContext::new(400, 400)?; - init()?; + init(None)?; let surface = glfw_ctx.create_surface(400, 400, 1.0)?; let graphics = graphics_create(surface, 400, 400)?; diff --git a/examples/update_pixels.rs b/examples/update_pixels.rs index 60c7de9..709af94 100644 --- a/examples/update_pixels.rs +++ b/examples/update_pixels.rs @@ -19,7 +19,7 @@ fn main() { fn sketch() -> error::Result<()> { let mut glfw_ctx = GlfwContext::new(100, 100)?; - init()?; + init(None)?; let width = 100; let height = 100; diff --git a/src/prelude.rs b/src/prelude.rs index c3ba153..312acb0 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,2 +1,2 @@ pub use bevy::prelude::default; -pub use processing_render::{render::command::DrawCommand, *}; +pub use processing_render::{config::*, render::command::DrawCommand, *};