Skip to content

Commit 3e8e10d

Browse files
committed
Add basic pyo3 module functionality / wrappers.
1 parent 060cd70 commit 3e8e10d

File tree

8 files changed

+387
-37
lines changed

8 files changed

+387
-37
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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ 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", features = ["static-link"] }
1718

1819
[target.'cfg(target_os = "linux")'.dependencies]
19-
glfw = { version = "0.60.0", features = ["wayland"] }
20+
glfw = { version = "0.60.0", features = ["static-link", "wayland"] }
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: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
use bevy::prelude::Entity;
2+
use processing::prelude::*;
3+
use pyo3::exceptions::PyRuntimeError;
4+
use pyo3::prelude::*;
5+
use pyo3::types::PyAny;
6+
use std::cell::RefCell;
7+
8+
use crate::glfw::GlfwContext;
9+
10+
// The global graphics context
11+
// we use thread_local! to ensure that the context is specific to the current thread, particularly
12+
// becausae glfw requires that all window operations happen on the main thread.
13+
// TODO: we'll want to think about how to enable multi-window / multi-threaded usage in the future
14+
thread_local! {
15+
static GRAPHICS_CTX: RefCell<Option<Py<Graphics>>> = const { RefCell::new(None) };
16+
}
17+
18+
#[pyclass(unsendable)]
19+
pub struct Graphics {
20+
// this makes our object !Send, hence unsendable above
21+
glfw_ctx: GlfwContext,
22+
surface: Entity,
23+
}
24+
25+
#[pymethods]
26+
impl Graphics {
27+
// TODO: in theory users can create multiple Graphics objects, which they manually manage themselves.
28+
// right now we just support the single global window via the module-level functions.
29+
#[new]
30+
fn new(width: u32, height: u32) -> PyResult<Self> {
31+
let glfw_ctx = GlfwContext::new(width, height)
32+
.map_err(|e| PyRuntimeError::new_err(format!("Couold not create window {e}")))?;
33+
34+
init().map_err(|e| PyRuntimeError::new_err(format!("Failed to initialize processing {e}")))?;
35+
36+
let window_handle = glfw_ctx.get_window();
37+
let display_handle = glfw_ctx.get_display();
38+
let surface = surface_create(window_handle, display_handle, width, height, 1.0)
39+
.map_err(|e| PyRuntimeError::new_err(format!("Could not create surface {e}")))?;
40+
41+
Ok(Self { glfw_ctx, surface })
42+
}
43+
44+
pub fn background(&self, args: Vec<f32>) -> PyResult<()> {
45+
let (r, g, b, a) = parse_color(&args)?;
46+
let color = bevy::color::Color::srgba(r, g, b, a);
47+
record_command(self.surface, DrawCommand::BackgroundColor(color))
48+
.map_err(|e| PyRuntimeError::new_err(format!("background failed {e}")))
49+
}
50+
51+
pub fn fill(&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::Fill(color))
55+
.map_err(|e| PyRuntimeError::new_err(format!("fill failed {e}")))
56+
}
57+
58+
pub fn no_fill(&self) -> PyResult<()> {
59+
record_command(self.surface, DrawCommand::NoFill)
60+
.map_err(|e| PyRuntimeError::new_err(format!("no_fill failed {e}")))
61+
}
62+
63+
pub fn stroke(&self, args: Vec<f32>) -> PyResult<()> {
64+
let (r, g, b, a) = parse_color(&args)?;
65+
let color = bevy::color::Color::srgba(r, g, b, a);
66+
record_command(self.surface, DrawCommand::StrokeColor(color))
67+
.map_err(|e| PyRuntimeError::new_err(format!("stroke failed {e}")))
68+
}
69+
70+
pub fn no_stroke(&self) -> PyResult<()> {
71+
record_command(self.surface, DrawCommand::NoStroke)
72+
.map_err(|e| PyRuntimeError::new_err(format!("no_stroke failed {e}")))
73+
}
74+
75+
pub fn stroke_weight(&self, weight: f32) -> PyResult<()> {
76+
record_command(self.surface, DrawCommand::StrokeWeight(weight))
77+
.map_err(|e| PyRuntimeError::new_err(format!("stroke_weight failed {e}")))
78+
}
79+
80+
pub fn rect(&self, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
81+
record_command(
82+
self.surface,
83+
DrawCommand::Rect {
84+
x,
85+
y,
86+
w,
87+
h,
88+
radii: [tl, tr, br, bl],
89+
},
90+
)
91+
.map_err(|e| PyRuntimeError::new_err(format!("rect failed {e}")))
92+
}
93+
94+
pub fn run(&mut self, draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
95+
loop {
96+
let running = self.glfw_ctx.poll_events();
97+
if !running {
98+
break;
99+
}
100+
101+
begin_draw(self.surface)
102+
.map_err(|e| PyRuntimeError::new_err(format!("begin_draw failed {e}")))?;
103+
104+
if let Some(ref draw) = draw_fn {
105+
Python::attach(|py| {
106+
draw.call0(py)
107+
.map_err(|e| PyRuntimeError::new_err(format!("draw failed {e}")))
108+
})?;
109+
}
110+
111+
end_draw(self.surface)
112+
.map_err(|e| PyRuntimeError::new_err(format!("end_draw failed {e}")))?;
113+
}
114+
115+
Ok(())
116+
}
117+
}
118+
119+
// TODO: a real color type. or color parser? idk. color is confusing. let's think
120+
// about how to expose different color spaces in an idiomatic pythonic way
121+
fn parse_color(args: &[f32]) -> PyResult<(f32, f32, f32, f32)> {
122+
match args.len() {
123+
4 => Ok((
124+
args[0] / 255.0,
125+
args[1] / 255.0,
126+
args[2] / 255.0,
127+
args[3] / 255.0,
128+
)),
129+
_ => Err(PyRuntimeError::new_err(
130+
"Color requires 4 arguments",
131+
)),
132+
}
133+
}
134+
135+
/// Run inside the current graphics context
136+
pub fn with_graphics<F, T>(f: F) -> PyResult<T>
137+
where
138+
F: FnOnce(PyRef<'_, Graphics>) -> PyResult<T>,
139+
{
140+
GRAPHICS_CTX.with(|cell| {
141+
let opt = cell.borrow();
142+
match opt.as_ref() {
143+
Some(py_graphics) => Python::attach(|py| {
144+
let graphics = py_graphics.bind(py).borrow();
145+
f(graphics)
146+
}),
147+
None => Err(PyRuntimeError::new_err(
148+
"No graphics context",
149+
)),
150+
}
151+
})
152+
}
153+
154+
/// Run inside the current graphics context with mutable access
155+
pub fn with_graphics_mut<F, T>(f: F) -> PyResult<T>
156+
where
157+
F: FnOnce(PyRefMut<'_, Graphics>) -> PyResult<T>,
158+
{
159+
GRAPHICS_CTX.with(|cell| {
160+
let opt = cell.borrow();
161+
match opt.as_ref() {
162+
Some(py_graphics) => Python::attach(|py| {
163+
let graphics = py_graphics.bind(py).borrow_mut();
164+
f(graphics)
165+
}),
166+
None => Err(PyRuntimeError::new_err(
167+
"No graphics context",
168+
)),
169+
}
170+
})
171+
}
172+
173+
/// Create the module level graphics context
174+
pub fn create_context(width: u32, height: u32) -> PyResult<()> {
175+
let already_exists = GRAPHICS_CTX.with(|cell| cell.borrow().is_some());
176+
if already_exists {
177+
return Err(PyRuntimeError::new_err("A context already exists"));
178+
}
179+
180+
Python::attach(|py| {
181+
let graphics = Py::new(py, Graphics::new(width, height)?)?;
182+
GRAPHICS_CTX.with(|cell| {
183+
*cell.borrow_mut() = Some(graphics);
184+
});
185+
Ok(())
186+
})
187+
}

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+
mod graphics;
12
mod glfw;
3+
24
use pyo3::prelude::*;
5+
use crate::graphics::{with_graphics, with_graphics_mut};
36

47
#[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-
}
8+
fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> {
9+
m.add_class::<graphics::Graphics>()?;
10+
11+
// settings / lifecycle
12+
m.add_function(wrap_pyfunction!(size, m)?)?;
13+
m.add_function(wrap_pyfunction!(run, m)?)?;
14+
15+
// draw state
16+
m.add_function(wrap_pyfunction!(background, m)?)?;
17+
m.add_function(wrap_pyfunction!(fill, m)?)?;
18+
m.add_function(wrap_pyfunction!(no_fill, m)?)?;
19+
m.add_function(wrap_pyfunction!(stroke, m)?)?;
20+
m.add_function(wrap_pyfunction!(no_stroke, m)?)?;
21+
m.add_function(wrap_pyfunction!(stroke_weight, m)?)?;
22+
23+
// drawing prims
24+
m.add_function(wrap_pyfunction!(rect, m)?)?;
25+
26+
27+
Ok(())
28+
}
29+
30+
// these are all our module-level functions
31+
//
32+
// in processing4 java, the sketch runs implicitly inside a class that extends PApplet and
33+
// executes main. here we have do a little magic trick to get similar behavior. if we required
34+
// users to create a Graphics object and call its methods, it would be ugly because we lack
35+
// an implicit receiver like 'this' in java. so instead we create a singleton Graphics object
36+
// behind the scenes and have these module-level functions forward to that object.
37+
38+
#[pyfunction]
39+
fn size(width: u32, height: u32) -> PyResult<()> {
40+
graphics::create_context(width, height)
41+
}
42+
43+
#[pyfunction]
44+
#[pyo3(signature = (draw_fn=None))]
45+
fn run(draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
46+
with_graphics_mut(|mut g| g.run(draw_fn))
47+
}
48+
49+
50+
#[pyfunction]
51+
#[pyo3(signature = (*args))]
52+
fn background(args: Vec<f32>) -> PyResult<()> {
53+
with_graphics(|g| g.background(args.to_vec()))
54+
}
55+
56+
#[pyfunction]
57+
#[pyo3(signature = (*args))]
58+
fn fill(args: Vec<f32>) -> PyResult<()> {
59+
with_graphics(|g| g.fill(args.to_vec()))
60+
}
61+
62+
#[pyfunction]
63+
fn no_fill() -> PyResult<()> {
64+
with_graphics(|g| g.no_fill())
65+
}
66+
67+
#[pyfunction]
68+
#[pyo3(signature = (*args))]
69+
fn stroke(args: Vec<f32>) -> PyResult<()> {
70+
with_graphics(|g| g.stroke(args.to_vec()))
71+
}
72+
73+
#[pyfunction]
74+
fn no_stroke() -> PyResult<()> {
75+
with_graphics(|g| g.no_stroke())
76+
}
77+
78+
#[pyfunction]
79+
fn stroke_weight(weight: f32) -> PyResult<()> {
80+
with_graphics(|g| g.stroke_weight(weight))
81+
}
82+
83+
#[pyfunction]
84+
#[pyo3(signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))]
85+
fn rect(x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
86+
with_graphics(|g| g.rect(x, y, w, h, tl, tr, br, bl))
4087
}

0 commit comments

Comments
 (0)