Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/processing_pyo3/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ crate-type = ["cdylib"]
[dependencies]
pyo3 = "0.27.0"
processing = { workspace = true }
glfw = "0.60.0"
bevy = { workspace = true }
glfw = { version = "0.60.0"}

[target.'cfg(target_os = "macos")'.dependencies]
glfw = { version = "0.60.0", features = ["static-link"] }

[target.'cfg(target_os = "linux")'.dependencies]
glfw = { version = "0.60.0", features = ["wayland"] }
5 changes: 5 additions & 0 deletions crates/processing_pyo3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Prototype for python bindings to libprocessing
### Install venv and maturin
Follow these [installation instructions](https://pyo3.rs/v0.27.2/getting-started.html)

#### macOS
```bash
brew install glfw
```

### Running code
```
$ maturin develop
Expand Down
15 changes: 15 additions & 0 deletions crates/processing_pyo3/examples/rectangle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from processing import *

# TODO: this should be in a setup function
size(800, 600)

def draw():
background(220)

fill(255, 0, 100)
stroke(0)
stroke_weight(2)
rect(100, 100, 200, 150)

# TODO: this should happen implicitly on module load somehow
run(draw)
3 changes: 3 additions & 0 deletions crates/processing_pyo3/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ classifiers = [
]
dynamic = ["version"]

[dependency-groups]
dev = ["maturin>=1.10,<2.0"]

[tool.maturin]
manifest-path = "Cargo.toml"
132 changes: 132 additions & 0 deletions crates/processing_pyo3/src/graphics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use bevy::prelude::Entity;
use processing::prelude::*;
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use pyo3::types::PyAny;

use crate::glfw::GlfwContext;

#[pyclass(unsendable)]
pub struct Graphics {
glfw_ctx: GlfwContext,
surface: Entity,
}

#[pymethods]
impl Graphics {
#[new]
pub fn new(width: u32, height: u32) -> PyResult<Self> {
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 window_handle = glfw_ctx.get_window();
let display_handle = glfw_ctx.get_display();
let surface = surface_create(window_handle, display_handle, width, height, 1.0)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;

Ok(Self { glfw_ctx, surface })
}

pub fn background(&self, args: Vec<f32>) -> PyResult<()> {
let (r, g, b, a) = parse_color(&args)?;
let color = bevy::color::Color::srgba(r, g, b, a);
record_command(self.surface, DrawCommand::BackgroundColor(color))
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn fill(&self, args: Vec<f32>) -> PyResult<()> {
let (r, g, b, a) = parse_color(&args)?;
let color = bevy::color::Color::srgba(r, g, b, a);
record_command(self.surface, DrawCommand::Fill(color))
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn no_fill(&self) -> PyResult<()> {
record_command(self.surface, DrawCommand::NoFill)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn stroke(&self, args: Vec<f32>) -> PyResult<()> {
let (r, g, b, a) = parse_color(&args)?;
let color = bevy::color::Color::srgba(r, g, b, a);
record_command(self.surface, DrawCommand::StrokeColor(color))
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn no_stroke(&self) -> PyResult<()> {
record_command(self.surface, DrawCommand::NoStroke)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn stroke_weight(&self, weight: f32) -> PyResult<()> {
record_command(self.surface, DrawCommand::StrokeWeight(weight))
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn rect(&self, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
record_command(
self.surface,
DrawCommand::Rect { x, y, w, h, radii: [tl, tr, br, bl] },
)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn run(&mut self, draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
loop {
if !self.glfw_ctx.poll_events() {
break;
}

begin_draw(self.surface)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;

if let Some(ref draw) = draw_fn {
Python::attach(|py| {
draw.call0(py).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
})?;
}

end_draw(self.surface)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
}
Ok(())
}
}

// 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<PyRef<'py, Graphics>> {
module
.getattr("_graphics")?
.cast_into::<Graphics>()
.map_err(|_| PyRuntimeError::new_err("no graphics context"))?
.try_borrow()
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

pub fn get_graphics_mut<'py>(module: &Bound<'py, PyModule>) -> PyResult<PyRefMut<'py, Graphics>> {
module
.getattr("_graphics")?
.cast_into::<Graphics>()
.map_err(|_| PyRuntimeError::new_err("no graphics context"))?
.try_borrow_mut()
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}
117 changes: 82 additions & 35 deletions crates/processing_pyo3/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,87 @@
//! # processing_pyo3
//!
//! A Python module that exposes libprocessing using pyo3.

//! In processing4 Java, the sketch runs implicitly inside a class that extends PApplet and
//! executes main. This means that all PAplet methods can be called directly without an explicit
//! receiver.
//!
//! To allow Python users to create a similar experience, we provide module-level
//! functions that forward to a singleton Graphics object behind the scenes.
mod glfw;
mod graphics;

use graphics::{get_graphics, get_graphics_mut, Graphics};
use pyo3::prelude::*;
use pyo3::types::PyAny;

#[pymodule]
mod processing {
use crate::glfw::GlfwContext;
use processing::prelude::*;
use pyo3::prelude::*;

/// create surface
#[pyfunction]
fn size(width: u32, height: u32) -> PyResult<String> {
let mut glfw_ctx = GlfwContext::new(400, 400).unwrap();
init().unwrap();

let window_handle = glfw_ctx.get_window();
let display_handle = glfw_ctx.get_display();
let surface = surface_create(window_handle, display_handle, width, height, 1.0).unwrap();

while glfw_ctx.poll_events() {
begin_draw(surface).unwrap();

record_command(
surface,
DrawCommand::Rect {
x: 10.0,
y: 10.0,
w: 100.0,
h: 100.0,
radii: [0.0, 0.0, 0.0, 0.0],
},
)
.unwrap();

end_draw(surface).unwrap();
}

Ok("OK".to_string())
}
fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Graphics>()?;
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(())
}

#[pyfunction]
#[pyo3(pass_module)]
fn size(module: &Bound<'_, PyModule>, width: u32, height: u32) -> PyResult<()> {
let graphics = Graphics::new(width, height)?;
module.setattr("_graphics", graphics)?;
Ok(())
}

#[pyfunction]
#[pyo3(pass_module, signature = (draw_fn=None))]
fn run(module: &Bound<'_, PyModule>, draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
get_graphics_mut(module)?.run(draw_fn)
}

#[pyfunction]
#[pyo3(pass_module, signature = (*args))]
fn background(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
get_graphics(module)?.background(args)
}

#[pyfunction]
#[pyo3(pass_module, signature = (*args))]
fn fill(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
get_graphics(module)?.fill(args)
}

#[pyfunction]
#[pyo3(pass_module)]
fn no_fill(module: &Bound<'_, PyModule>) -> PyResult<()> {
get_graphics(module)?.no_fill()
}

#[pyfunction]
#[pyo3(pass_module, signature = (*args))]
fn stroke(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
get_graphics(module)?.stroke(args)
}

#[pyfunction]
#[pyo3(pass_module)]
fn no_stroke(module: &Bound<'_, PyModule>) -> PyResult<()> {
get_graphics(module)?.no_stroke()
}

#[pyfunction]
#[pyo3(pass_module)]
fn stroke_weight(module: &Bound<'_, PyModule>, weight: f32) -> PyResult<()> {
get_graphics(module)?.stroke_weight(weight)
}

#[pyfunction]
#[pyo3(pass_module, signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))]
fn rect(module: &Bound<'_, PyModule>, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
get_graphics(module)?.rect(x, y, w, h, tl, tr, br, bl)
}
Loading
Loading