Skip to content

Commit 3c476d1

Browse files
authored
Image grab bag (#155)
1 parent 4097598 commit 3c476d1

11 files changed

Lines changed: 718 additions & 90 deletions

File tree

crates/processing_pyo3/src/graphics.rs

Lines changed: 185 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,66 @@ impl PyBlendMode {
132132
const OP_MAX: u8 = 4;
133133
}
134134

135+
/// Configures how an image is sampled when drawn.
136+
///
137+
/// Controls texture filtering and edge wrapping behavior.
138+
///
139+
/// - `filter` — `Sampler.LINEAR` (smooth) or `Sampler.NEAREST` (pixelated).
140+
/// - `wrap` — `Sampler.CLAMP` (default), `Sampler.REPEAT`, or `Sampler.MIRROR`.
141+
/// Use `wrap_x`/`wrap_y` to set each axis independently.
142+
#[pyclass(from_py_object)]
143+
#[derive(Clone)]
144+
pub struct Sampler {
145+
pub(crate) filter: u8,
146+
pub(crate) wrap_x: u8,
147+
pub(crate) wrap_y: u8,
148+
}
149+
150+
#[pymethods]
151+
impl Sampler {
152+
#[new]
153+
#[pyo3(signature = (*, filter=0, wrap=0, wrap_x=None, wrap_y=None))]
154+
fn new(filter: u8, wrap: u8, wrap_x: Option<u8>, wrap_y: Option<u8>) -> Self {
155+
Self {
156+
filter,
157+
wrap_x: wrap_x.unwrap_or(wrap),
158+
wrap_y: wrap_y.unwrap_or(wrap),
159+
}
160+
}
161+
162+
fn __repr__(&self) -> String {
163+
let filter_name = match self.filter {
164+
0 => "LINEAR",
165+
1 => "NEAREST",
166+
_ => "?",
167+
};
168+
let wrap_name = |v: u8| match v {
169+
0 => "CLAMP",
170+
1 => "REPEAT",
171+
2 => "MIRROR",
172+
_ => "?",
173+
};
174+
format!(
175+
"Sampler(filter={}, wrap_x={}, wrap_y={})",
176+
filter_name,
177+
wrap_name(self.wrap_x),
178+
wrap_name(self.wrap_y)
179+
)
180+
}
181+
182+
#[classattr]
183+
const LINEAR: u8 = 0;
184+
#[classattr]
185+
const NEAREST: u8 = 1;
186+
187+
#[classattr]
188+
const CLAMP: u8 = 0;
189+
#[classattr]
190+
const REPEAT: u8 = 1;
191+
#[classattr]
192+
const MIRROR: u8 = 2;
193+
}
194+
135195
pub use crate::surface::Surface;
136196

137197
#[pyclass]
@@ -168,10 +228,40 @@ pub struct Image {
168228
pub(crate) entity: Entity,
169229
}
170230

231+
pub(crate) struct ImageRef {
232+
pub entity: Entity,
233+
}
234+
235+
impl<'a, 'py> FromPyObject<'a, 'py> for ImageRef {
236+
type Error = PyErr;
237+
238+
fn extract(ob: pyo3::Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
239+
if let Ok(img) = ob.extract::<PyRef<Image>>() {
240+
return Ok(ImageRef { entity: img.entity });
241+
}
242+
#[cfg(feature = "webcam")]
243+
if let Ok(cam) = ob.extract::<PyRef<crate::webcam::Webcam>>() {
244+
return Ok(ImageRef {
245+
entity: cam.image_entity()?,
246+
});
247+
}
248+
Err(pyo3::exceptions::PyTypeError::new_err(
249+
"expected an Image or Webcam",
250+
))
251+
}
252+
}
253+
254+
#[pymethods]
171255
impl Image {
172-
#[expect(dead_code)] // it's only used by webcam atm
173-
pub(crate) fn from_entity(entity: Entity) -> Self {
174-
Self { entity }
256+
/// Applies a `Sampler` to this image, controlling filtering and wrapping.
257+
///
258+
/// ```python
259+
/// s = Sampler(filter=Sampler.NEAREST, wrap=Sampler.REPEAT)
260+
/// img.sampler(s)
261+
/// ```
262+
fn sampler(&self, sampler: &Sampler) -> PyResult<()> {
263+
image_set_sampler(self.entity, sampler.filter, sampler.wrap_x, sampler.wrap_y)
264+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
175265
}
176266
}
177267

@@ -785,13 +875,89 @@ impl Graphics {
785875
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
786876
}
787877

788-
pub fn image(&self, file: &str) -> PyResult<Image> {
878+
/// Loads an image from a file and returns an Image object.
879+
///
880+
/// The path is relative to the sketch's assets directory.
881+
pub fn load_image(&self, file: &str) -> PyResult<Image> {
789882
match image_load(file) {
790883
Ok(image) => Ok(Image { entity: image }),
791884
Err(e) => Err(PyRuntimeError::new_err(format!("{e}"))),
792885
}
793886
}
794887

888+
/// Draws an image to the screen.
889+
///
890+
/// Optional `d_width` and `d_height` resize the image on screen. If omitted,
891+
/// the image's original dimensions are used.
892+
///
893+
/// Optional `sx`, `sy`, `s_width`, and `s_height` define a sub-region
894+
/// of the source image to draw, specified in pixels.
895+
///
896+
/// Affected by `image_mode()`, `tint()`, and the current transform.
897+
#[pyo3(signature = (source, dx, dy, d_width=None, d_height=None, sx=None, sy=None, s_width=None, s_height=None))]
898+
pub fn image(
899+
&self,
900+
source: ImageRef,
901+
dx: f32,
902+
dy: f32,
903+
d_width: Option<f32>,
904+
d_height: Option<f32>,
905+
sx: Option<f32>,
906+
sy: Option<f32>,
907+
s_width: Option<f32>,
908+
s_height: Option<f32>,
909+
) -> PyResult<()> {
910+
graphics_record_command(
911+
self.entity,
912+
DrawCommand::Image {
913+
entity: source.entity,
914+
dx,
915+
dy,
916+
d_width,
917+
d_height,
918+
sx,
919+
sy,
920+
s_width,
921+
s_height,
922+
},
923+
)
924+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
925+
}
926+
927+
/// Sets a tint color applied when drawing images.
928+
///
929+
/// Accepts the same color arguments as `fill()`. The tint is multiplied
930+
/// with the image's pixel colors. Use `no_tint()` to remove.
931+
#[pyo3(signature = (*args))]
932+
pub fn tint(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> {
933+
let color = extract_color_with_mode(
934+
args,
935+
&graphics_get_color_mode(self.entity)
936+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?,
937+
)?;
938+
graphics_record_command(self.entity, DrawCommand::Tint(color))
939+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
940+
}
941+
942+
/// Removes the current tint color so images draw without color modification.
943+
pub fn no_tint(&self) -> PyResult<()> {
944+
graphics_record_command(self.entity, DrawCommand::NoTint)
945+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
946+
}
947+
948+
/// Changes how image position arguments are interpreted.
949+
///
950+
/// - `CORNER` (default) — `dx`, `dy` is the top-left corner.
951+
/// - `CORNERS` — `dx`, `dy` and `d_width`, `d_height` are opposite corners.
952+
/// - `CENTER` — `dx`, `dy` is the center of the image.
953+
pub fn image_mode(&self, mode: u8) -> PyResult<()> {
954+
graphics_record_command(
955+
self.entity,
956+
DrawCommand::ImageMode(processing::prelude::ShapeMode::from(mode)),
957+
)
958+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
959+
}
960+
795961
pub fn create_image(&self, width: u32, height: u32) -> PyResult<Image> {
796962
let size = Extent3d {
797963
width,
@@ -831,6 +997,21 @@ impl Graphics {
831997
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
832998
}
833999

1000+
pub fn rotate_x(&self, angle: f32) -> PyResult<()> {
1001+
graphics_record_command(self.entity, DrawCommand::RotateX { angle })
1002+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1003+
}
1004+
1005+
pub fn rotate_y(&self, angle: f32) -> PyResult<()> {
1006+
graphics_record_command(self.entity, DrawCommand::RotateY { angle })
1007+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1008+
}
1009+
1010+
pub fn rotate_z(&self, angle: f32) -> PyResult<()> {
1011+
graphics_record_command(self.entity, DrawCommand::RotateZ { angle })
1012+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1013+
}
1014+
8341015
pub fn draw_box(&self, width: f32, height: f32, depth: f32) -> PyResult<()> {
8351016
graphics_record_command(
8361017
self.entity,

crates/processing_pyo3/src/lib.rs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ mod time;
2626
mod webcam;
2727

2828
use graphics::{
29-
Geometry, Graphics, Image, Light, PyBlendMode, Topology, get_graphics, get_graphics_mut,
29+
Geometry, Graphics, Image, Light, PyBlendMode, Sampler, Topology, get_graphics,
30+
get_graphics_mut,
3031
};
3132
use material::Material;
3233

@@ -340,6 +341,8 @@ mod mewnala {
340341
#[pymodule_export]
341342
use super::PyBlendMode;
342343
#[pymodule_export]
344+
use super::Sampler;
345+
#[pymodule_export]
343346
use super::Shader;
344347
#[pymodule_export]
345348
use super::Topology;
@@ -623,6 +626,10 @@ mod mewnala {
623626
mod math {
624627
use super::*;
625628

629+
#[pymodule_export]
630+
use crate::math::PyAffine2;
631+
#[pymodule_export]
632+
use crate::math::PyMat2;
626633
#[pymodule_export]
627634
use crate::math::PyQuat;
628635
#[pymodule_export]
@@ -1236,6 +1243,24 @@ mod mewnala {
12361243
graphics!(module).rotate(angle)
12371244
}
12381245

1246+
#[pyfunction]
1247+
#[pyo3(pass_module)]
1248+
fn rotate_x(module: &Bound<'_, PyModule>, angle: f32) -> PyResult<()> {
1249+
graphics!(module).rotate_x(angle)
1250+
}
1251+
1252+
#[pyfunction]
1253+
#[pyo3(pass_module)]
1254+
fn rotate_y(module: &Bound<'_, PyModule>, angle: f32) -> PyResult<()> {
1255+
graphics!(module).rotate_y(angle)
1256+
}
1257+
1258+
#[pyfunction]
1259+
#[pyo3(pass_module)]
1260+
fn rotate_z(module: &Bound<'_, PyModule>, angle: f32) -> PyResult<()> {
1261+
graphics!(module).rotate_z(angle)
1262+
}
1263+
12391264
#[pyfunction(name = "box")]
12401265
#[pyo3(pass_module, signature = (*args))]
12411266
fn draw_box(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> {
@@ -1394,12 +1419,61 @@ mod mewnala {
13941419
graphics!(module).rect(x, y, w, h, tl, tr, br, bl)
13951420
}
13961421

1422+
/// Loads an image from a file and returns an Image object.
13971423
#[pyfunction]
13981424
#[pyo3(pass_module, signature = (image_file))]
1399-
fn image(module: &Bound<'_, PyModule>, image_file: &str) -> PyResult<Image> {
1425+
fn load_image(module: &Bound<'_, PyModule>, image_file: &str) -> PyResult<Image> {
14001426
let graphics =
14011427
get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?;
1402-
graphics.image(image_file)
1428+
graphics.load_image(image_file)
1429+
}
1430+
1431+
/// Draws an image to the screen.
1432+
///
1433+
/// Optional `d_width`/`d_height` resize on screen; defaults to the image's
1434+
/// original dimensions. Optional `sx`/`sy`/`s_width`/`s_height` select a
1435+
/// sub-region of the source image in pixels.
1436+
#[pyfunction]
1437+
#[pyo3(pass_module, signature = (source, dx, dy, d_width=None, d_height=None, sx=None, sy=None, s_width=None, s_height=None))]
1438+
#[allow(clippy::too_many_arguments)]
1439+
fn image(
1440+
module: &Bound<'_, PyModule>,
1441+
source: graphics::ImageRef,
1442+
dx: f32,
1443+
dy: f32,
1444+
d_width: Option<f32>,
1445+
d_height: Option<f32>,
1446+
sx: Option<f32>,
1447+
sy: Option<f32>,
1448+
s_width: Option<f32>,
1449+
s_height: Option<f32>,
1450+
) -> PyResult<()> {
1451+
graphics!(module).image(source, dx, dy, d_width, d_height, sx, sy, s_width, s_height)
1452+
}
1453+
1454+
/// Sets a tint color applied when drawing images.
1455+
#[pyfunction]
1456+
#[pyo3(pass_module, signature = (*args))]
1457+
fn tint(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> {
1458+
graphics!(module).tint(args)
1459+
}
1460+
1461+
/// Removes the current tint so images draw without color modification.
1462+
#[pyfunction]
1463+
#[pyo3(pass_module)]
1464+
fn no_tint(module: &Bound<'_, PyModule>) -> PyResult<()> {
1465+
graphics!(module).no_tint()
1466+
}
1467+
1468+
/// Changes how image position arguments are interpreted.
1469+
///
1470+
/// - `CORNER` (default) — `dx`, `dy` is the top-left corner.
1471+
/// - `CENTER` — `dx`, `dy` is the center.
1472+
/// - `CORNERS` — `dx`, `dy` and `d_width`, `d_height` are opposite corners.
1473+
#[pyfunction]
1474+
#[pyo3(pass_module)]
1475+
fn image_mode(module: &Bound<'_, PyModule>, mode: u8) -> PyResult<()> {
1476+
graphics!(module).image_mode(mode)
14031477
}
14041478

14051479
#[pyfunction]

crates/processing_pyo3/src/material.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use processing::prelude::*;
33
use pyo3::types::PyDict;
44
use pyo3::{exceptions::PyRuntimeError, prelude::*};
55

6+
use crate::graphics::ImageRef;
67
use crate::math::{PyVec2, PyVec3, PyVec4};
78
use crate::shader::Shader;
89

@@ -12,6 +13,9 @@ pub struct Material {
1213
}
1314

1415
fn py_to_material_value(value: &Bound<'_, PyAny>) -> PyResult<material::MaterialValue> {
16+
if let Ok(img_ref) = value.extract::<ImageRef>() {
17+
return Ok(material::MaterialValue::Texture(img_ref.entity));
18+
}
1519
if let Ok(v) = value.extract::<f32>() {
1620
return Ok(material::MaterialValue::Float(v));
1721
}

0 commit comments

Comments
 (0)