Skip to content

Commit 553ec04

Browse files
authored
Add basic pyo3 module functionality / wrappers (#35)
1 parent 060cd70 commit 553ec04

File tree

9 files changed

+339
-36
lines changed

9 files changed

+339
-36
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/processing_pyo3/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ crate-type = ["cdylib"]
1313
[dependencies]
1414
pyo3 = "0.27.0"
1515
processing = { workspace = true }
16-
glfw = "0.60.0"
16+
bevy = { workspace = true }
17+
glfw = { version = "0.60.0"}
18+
19+
[target.'cfg(target_os = "macos")'.dependencies]
20+
glfw = { version = "0.60.0", features = ["static-link"] }
1721

1822
[target.'cfg(target_os = "linux")'.dependencies]
1923
glfw = { version = "0.60.0", features = ["wayland"] }

crates/processing_pyo3/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ Prototype for python bindings to libprocessing
77
### Install venv and maturin
88
Follow these [installation instructions](https://pyo3.rs/v0.27.2/getting-started.html)
99

10+
#### macOS
11+
```bash
12+
brew install glfw
13+
```
14+
1015
### Running code
1116
```
1217
$ maturin develop
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from processing import *
2+
3+
# TODO: this should be in a setup function
4+
size(800, 600)
5+
6+
def draw():
7+
background(220)
8+
9+
fill(255, 0, 100)
10+
stroke(0)
11+
stroke_weight(2)
12+
rect(100, 100, 200, 150)
13+
14+
# TODO: this should happen implicitly on module load somehow
15+
run(draw)

crates/processing_pyo3/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ classifiers = [
1212
]
1313
dynamic = ["version"]
1414

15+
[dependency-groups]
16+
dev = ["maturin>=1.10,<2.0"]
17+
1518
[tool.maturin]
1619
manifest-path = "Cargo.toml"
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use bevy::prelude::Entity;
2+
use processing::prelude::*;
3+
use pyo3::exceptions::PyRuntimeError;
4+
use pyo3::prelude::*;
5+
use pyo3::types::PyAny;
6+
7+
use crate::glfw::GlfwContext;
8+
9+
#[pyclass(unsendable)]
10+
pub struct Graphics {
11+
glfw_ctx: GlfwContext,
12+
surface: Entity,
13+
}
14+
15+
#[pymethods]
16+
impl Graphics {
17+
#[new]
18+
pub fn new(width: u32, height: u32) -> PyResult<Self> {
19+
let glfw_ctx = GlfwContext::new(width, height)
20+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
21+
22+
init().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
23+
24+
let window_handle = glfw_ctx.get_window();
25+
let display_handle = glfw_ctx.get_display();
26+
let surface = surface_create(window_handle, display_handle, width, height, 1.0)
27+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
28+
29+
Ok(Self { glfw_ctx, surface })
30+
}
31+
32+
pub fn background(&self, args: Vec<f32>) -> PyResult<()> {
33+
let (r, g, b, a) = parse_color(&args)?;
34+
let color = bevy::color::Color::srgba(r, g, b, a);
35+
record_command(self.surface, DrawCommand::BackgroundColor(color))
36+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
37+
}
38+
39+
pub fn fill(&self, args: Vec<f32>) -> PyResult<()> {
40+
let (r, g, b, a) = parse_color(&args)?;
41+
let color = bevy::color::Color::srgba(r, g, b, a);
42+
record_command(self.surface, DrawCommand::Fill(color))
43+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
44+
}
45+
46+
pub fn no_fill(&self) -> PyResult<()> {
47+
record_command(self.surface, DrawCommand::NoFill)
48+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
49+
}
50+
51+
pub fn stroke(&self, args: Vec<f32>) -> PyResult<()> {
52+
let (r, g, b, a) = parse_color(&args)?;
53+
let color = bevy::color::Color::srgba(r, g, b, a);
54+
record_command(self.surface, DrawCommand::StrokeColor(color))
55+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
56+
}
57+
58+
pub fn no_stroke(&self) -> PyResult<()> {
59+
record_command(self.surface, DrawCommand::NoStroke)
60+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
61+
}
62+
63+
pub fn stroke_weight(&self, weight: f32) -> PyResult<()> {
64+
record_command(self.surface, DrawCommand::StrokeWeight(weight))
65+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
66+
}
67+
68+
pub fn rect(&self, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
69+
record_command(
70+
self.surface,
71+
DrawCommand::Rect { x, y, w, h, radii: [tl, tr, br, bl] },
72+
)
73+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
74+
}
75+
76+
pub fn run(&mut self, draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
77+
loop {
78+
if !self.glfw_ctx.poll_events() {
79+
break;
80+
}
81+
82+
begin_draw(self.surface)
83+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
84+
85+
if let Some(ref draw) = draw_fn {
86+
Python::attach(|py| {
87+
draw.call0(py).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
88+
})?;
89+
}
90+
91+
end_draw(self.surface)
92+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
93+
}
94+
Ok(())
95+
}
96+
}
97+
98+
// TODO: a real color type. or color parser? idk. color is confusing. let's think
99+
// about how to expose different color spaces in an idiomatic pythonic way
100+
fn parse_color(args: &[f32]) -> PyResult<(f32, f32, f32, f32)> {
101+
match args.len() {
102+
1 => {
103+
let v = args[0] / 255.0;
104+
Ok((v, v, v, 1.0))
105+
}
106+
2 => {
107+
let v = args[0] / 255.0;
108+
Ok((v, v, v, args[1] / 255.0))
109+
}
110+
3 => Ok((args[0] / 255.0, args[1] / 255.0, args[2] / 255.0, 1.0)),
111+
4 => Ok((args[0] / 255.0, args[1] / 255.0, args[2] / 255.0, args[3] / 255.0)),
112+
_ => Err(PyRuntimeError::new_err("color requires 1-4 arguments")),
113+
}
114+
}
115+
116+
pub fn get_graphics<'py>(module: &Bound<'py, PyModule>) -> PyResult<PyRef<'py, Graphics>> {
117+
module
118+
.getattr("_graphics")?
119+
.cast_into::<Graphics>()
120+
.map_err(|_| PyRuntimeError::new_err("no graphics context"))?
121+
.try_borrow()
122+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
123+
}
124+
125+
pub fn get_graphics_mut<'py>(module: &Bound<'py, PyModule>) -> PyResult<PyRefMut<'py, Graphics>> {
126+
module
127+
.getattr("_graphics")?
128+
.cast_into::<Graphics>()
129+
.map_err(|_| PyRuntimeError::new_err("no graphics context"))?
130+
.try_borrow_mut()
131+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
132+
}

crates/processing_pyo3/src/lib.rs

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,87 @@
1+
//! # processing_pyo3
2+
//!
3+
//! A Python module that exposes libprocessing using pyo3.
4+
5+
//! In processing4 Java, the sketch runs implicitly inside a class that extends PApplet and
6+
//! executes main. This means that all PAplet methods can be called directly without an explicit
7+
//! receiver.
8+
//!
9+
//! To allow Python users to create a similar experience, we provide module-level
10+
//! functions that forward to a singleton Graphics object behind the scenes.
111
mod glfw;
12+
mod graphics;
13+
14+
use graphics::{get_graphics, get_graphics_mut, Graphics};
215
use pyo3::prelude::*;
16+
use pyo3::types::PyAny;
317

418
#[pymodule]
5-
mod processing {
6-
use crate::glfw::GlfwContext;
7-
use processing::prelude::*;
8-
use pyo3::prelude::*;
9-
10-
/// create surface
11-
#[pyfunction]
12-
fn size(width: u32, height: u32) -> PyResult<String> {
13-
let mut glfw_ctx = GlfwContext::new(400, 400).unwrap();
14-
init().unwrap();
15-
16-
let window_handle = glfw_ctx.get_window();
17-
let display_handle = glfw_ctx.get_display();
18-
let surface = surface_create(window_handle, display_handle, width, height, 1.0).unwrap();
19-
20-
while glfw_ctx.poll_events() {
21-
begin_draw(surface).unwrap();
22-
23-
record_command(
24-
surface,
25-
DrawCommand::Rect {
26-
x: 10.0,
27-
y: 10.0,
28-
w: 100.0,
29-
h: 100.0,
30-
radii: [0.0, 0.0, 0.0, 0.0],
31-
},
32-
)
33-
.unwrap();
34-
35-
end_draw(surface).unwrap();
36-
}
37-
38-
Ok("OK".to_string())
39-
}
19+
fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> {
20+
m.add_class::<Graphics>()?;
21+
m.add_function(wrap_pyfunction!(size, m)?)?;
22+
m.add_function(wrap_pyfunction!(run, m)?)?;
23+
m.add_function(wrap_pyfunction!(background, m)?)?;
24+
m.add_function(wrap_pyfunction!(fill, m)?)?;
25+
m.add_function(wrap_pyfunction!(no_fill, m)?)?;
26+
m.add_function(wrap_pyfunction!(stroke, m)?)?;
27+
m.add_function(wrap_pyfunction!(no_stroke, m)?)?;
28+
m.add_function(wrap_pyfunction!(stroke_weight, m)?)?;
29+
m.add_function(wrap_pyfunction!(rect, m)?)?;
30+
Ok(())
31+
}
32+
33+
#[pyfunction]
34+
#[pyo3(pass_module)]
35+
fn size(module: &Bound<'_, PyModule>, width: u32, height: u32) -> PyResult<()> {
36+
let graphics = Graphics::new(width, height)?;
37+
module.setattr("_graphics", graphics)?;
38+
Ok(())
39+
}
40+
41+
#[pyfunction]
42+
#[pyo3(pass_module, signature = (draw_fn=None))]
43+
fn run(module: &Bound<'_, PyModule>, draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
44+
get_graphics_mut(module)?.run(draw_fn)
45+
}
46+
47+
#[pyfunction]
48+
#[pyo3(pass_module, signature = (*args))]
49+
fn background(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
50+
get_graphics(module)?.background(args)
51+
}
52+
53+
#[pyfunction]
54+
#[pyo3(pass_module, signature = (*args))]
55+
fn fill(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
56+
get_graphics(module)?.fill(args)
57+
}
58+
59+
#[pyfunction]
60+
#[pyo3(pass_module)]
61+
fn no_fill(module: &Bound<'_, PyModule>) -> PyResult<()> {
62+
get_graphics(module)?.no_fill()
63+
}
64+
65+
#[pyfunction]
66+
#[pyo3(pass_module, signature = (*args))]
67+
fn stroke(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
68+
get_graphics(module)?.stroke(args)
69+
}
70+
71+
#[pyfunction]
72+
#[pyo3(pass_module)]
73+
fn no_stroke(module: &Bound<'_, PyModule>) -> PyResult<()> {
74+
get_graphics(module)?.no_stroke()
75+
}
76+
77+
#[pyfunction]
78+
#[pyo3(pass_module)]
79+
fn stroke_weight(module: &Bound<'_, PyModule>, weight: f32) -> PyResult<()> {
80+
get_graphics(module)?.stroke_weight(weight)
81+
}
82+
83+
#[pyfunction]
84+
#[pyo3(pass_module, signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))]
85+
fn rect(module: &Bound<'_, PyModule>, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
86+
get_graphics(module)?.rect(x, y, w, h, tl, tr, br, bl)
4087
}

0 commit comments

Comments
 (0)