Skip to content

Commit 127ad0e

Browse files
committed
Merge main
2 parents f74f8eb + 02ffebd commit 127ad0e

15 files changed

Lines changed: 825 additions & 126 deletions

File tree

crates/processing_core/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ thread_local! {
1414
}
1515

1616
pub fn app_mut<T>(cb: impl FnOnce(&mut App) -> error::Result<T>) -> error::Result<T> {
17-
// `try_with` rather than `with` so callers (especially `Drop`s running
18-
// during pyo3 module teardown) get a graceful error instead of a panic
19-
// when the thread-local has already been destroyed.
17+
// `try_with` so a `Drop` running after the TLS is destroyed sees an
18+
// `AppAccess` error rather than panicking
2019
let res = APP.try_with(|app_cell| {
2120
let mut app_borrow = app_cell
2221
.try_borrow_mut()

crates/processing_glfw/src/lib.rs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -335,17 +335,26 @@ impl GlfwContext {
335335
if desired.maximize {
336336
self.window.maximize();
337337
}
338+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
338339
if desired.focus {
339340
self.window.focus();
340341
}
341342

342-
let (cx, cy) = self.window.get_pos();
343-
let (inset_l, inset_t, _, _) = self.window.get_frame_size();
344-
let frame_pos = IVec2::new(cx - inset_l, cy - inset_t);
343+
#[cfg(all(target_os = "linux", feature = "wayland"))]
344+
let frame_pos: Option<IVec2> = None;
345+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
346+
let frame_pos = {
347+
let (cx, cy) = self.window.get_pos();
348+
let (inset_l, inset_t, _, _) = self.window.get_frame_size();
349+
Some(IVec2::new(cx - inset_l, cy - inset_t))
350+
};
351+
345352
let _ = app_mut(|app| {
346353
let world = app.world_mut();
347-
if let Some(mut window) = world.get_mut::<BevyWindow>(surface) {
348-
window.position = WindowPosition::At(frame_pos);
354+
if let Some(pos) = frame_pos
355+
&& let Some(mut window) = world.get_mut::<BevyWindow>(surface)
356+
{
357+
window.position = WindowPosition::At(pos);
349358
}
350359
if let Some(mut controls) = world.get_mut::<WindowControls>(surface) {
351360
controls.pending_iconify = false;
@@ -355,7 +364,9 @@ impl GlfwContext {
355364
}
356365
Ok(())
357366
});
358-
self.last_applied.position = frame_pos;
367+
if let Some(pos) = frame_pos {
368+
self.last_applied.position = pos;
369+
}
359370
}
360371

361372
fn apply_window(&mut self, desired: &DesiredWindow) {
@@ -368,8 +379,11 @@ impl GlfwContext {
368379
if let Some(pos) = desired.position
369380
&& pos != last.position
370381
{
371-
let (inset_l, inset_t, _, _) = self.window.get_frame_size();
372-
self.window.set_pos(pos.x + inset_l, pos.y + inset_t);
382+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
383+
{
384+
let (inset_l, inset_t, _, _) = self.window.get_frame_size();
385+
self.window.set_pos(pos.x + inset_l, pos.y + inset_t);
386+
}
373387
last.position = pos;
374388
}
375389
if desired.size != last.size && desired.size.x > 0 && desired.size.y > 0 {
@@ -394,13 +408,15 @@ impl GlfwContext {
394408
last.decorations = desired.decorations;
395409
}
396410
if desired.window_level != last.window_level {
411+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
397412
self.window
398413
.set_floating(matches!(desired.window_level, BevyWindowLevel::AlwaysOnTop));
399414
last.window_level = desired.window_level;
400415
}
401416
if let Some(opacity) = desired.opacity
402417
&& (opacity - last.opacity).abs() > f32::EPSILON
403418
{
419+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
404420
self.window.set_opacity(opacity);
405421
last.opacity = opacity;
406422
}
@@ -413,6 +429,9 @@ impl GlfwContext {
413429
match target {
414430
Some(monitor_entity) => {
415431
if self.last_applied.fullscreen_on.is_none() {
432+
#[cfg(all(target_os = "linux", feature = "wayland"))]
433+
let (x, y) = (0, 0);
434+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
416435
let (x, y) = self.window.get_pos();
417436
let (w, h) = self.window.get_size();
418437
self.windowed_geometry = Some((x, y, w as u32, h as u32));
@@ -438,6 +457,9 @@ impl GlfwContext {
438457
}
439458
None => {
440459
let (x, y, w, h) = self.windowed_geometry.take().unwrap_or_else(|| {
460+
#[cfg(all(target_os = "linux", feature = "wayland"))]
461+
let (x, y) = (0, 0);
462+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
441463
let (x, y) = self.window.get_pos();
442464
let (w, h) = self.window.get_size();
443465
(x, y, w as u32, h as u32)
@@ -486,6 +508,7 @@ struct DesiredWindow {
486508
iconify: bool,
487509
restore: bool,
488510
maximize: bool,
511+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
489512
focus: bool,
490513
}
491514

@@ -524,6 +547,7 @@ fn read_desired_window(surface: Entity) -> Option<DesiredWindow> {
524547
iconify: controls.pending_iconify,
525548
restore: controls.pending_restore,
526549
maximize: controls.pending_maximize,
550+
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
527551
focus: controls.pending_focus,
528552
}))
529553
})

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

@@ -820,13 +910,89 @@ impl Graphics {
820910
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
821911
}
822912

823-
pub fn image(&self, file: &str) -> PyResult<Image> {
913+
/// Loads an image from a file and returns an Image object.
914+
///
915+
/// The path is relative to the sketch's assets directory.
916+
pub fn load_image(&self, file: &str) -> PyResult<Image> {
824917
match image_load(file) {
825918
Ok(image) => Ok(Image { entity: image }),
826919
Err(e) => Err(PyRuntimeError::new_err(format!("{e}"))),
827920
}
828921
}
829922

923+
/// Draws an image to the screen.
924+
///
925+
/// Optional `d_width` and `d_height` resize the image on screen. If omitted,
926+
/// the image's original dimensions are used.
927+
///
928+
/// Optional `sx`, `sy`, `s_width`, and `s_height` define a sub-region
929+
/// of the source image to draw, specified in pixels.
930+
///
931+
/// Affected by `image_mode()`, `tint()`, and the current transform.
932+
#[pyo3(signature = (source, dx, dy, d_width=None, d_height=None, sx=None, sy=None, s_width=None, s_height=None))]
933+
pub fn image(
934+
&self,
935+
source: ImageRef,
936+
dx: f32,
937+
dy: f32,
938+
d_width: Option<f32>,
939+
d_height: Option<f32>,
940+
sx: Option<f32>,
941+
sy: Option<f32>,
942+
s_width: Option<f32>,
943+
s_height: Option<f32>,
944+
) -> PyResult<()> {
945+
graphics_record_command(
946+
self.entity,
947+
DrawCommand::Image {
948+
entity: source.entity,
949+
dx,
950+
dy,
951+
d_width,
952+
d_height,
953+
sx,
954+
sy,
955+
s_width,
956+
s_height,
957+
},
958+
)
959+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
960+
}
961+
962+
/// Sets a tint color applied when drawing images.
963+
///
964+
/// Accepts the same color arguments as `fill()`. The tint is multiplied
965+
/// with the image's pixel colors. Use `no_tint()` to remove.
966+
#[pyo3(signature = (*args))]
967+
pub fn tint(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> {
968+
let color = extract_color_with_mode(
969+
args,
970+
&graphics_get_color_mode(self.entity)
971+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?,
972+
)?;
973+
graphics_record_command(self.entity, DrawCommand::Tint(color))
974+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
975+
}
976+
977+
/// Removes the current tint color so images draw without color modification.
978+
pub fn no_tint(&self) -> PyResult<()> {
979+
graphics_record_command(self.entity, DrawCommand::NoTint)
980+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
981+
}
982+
983+
/// Changes how image position arguments are interpreted.
984+
///
985+
/// - `CORNER` (default) — `dx`, `dy` is the top-left corner.
986+
/// - `CORNERS` — `dx`, `dy` and `d_width`, `d_height` are opposite corners.
987+
/// - `CENTER` — `dx`, `dy` is the center of the image.
988+
pub fn image_mode(&self, mode: u8) -> PyResult<()> {
989+
graphics_record_command(
990+
self.entity,
991+
DrawCommand::ImageMode(processing::prelude::ShapeMode::from(mode)),
992+
)
993+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
994+
}
995+
830996
pub fn create_image(&self, width: u32, height: u32) -> PyResult<Image> {
831997
let size = Extent3d {
832998
width,
@@ -866,6 +1032,21 @@ impl Graphics {
8661032
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
8671033
}
8681034

1035+
pub fn rotate_x(&self, angle: f32) -> PyResult<()> {
1036+
graphics_record_command(self.entity, DrawCommand::RotateX { angle })
1037+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1038+
}
1039+
1040+
pub fn rotate_y(&self, angle: f32) -> PyResult<()> {
1041+
graphics_record_command(self.entity, DrawCommand::RotateY { angle })
1042+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1043+
}
1044+
1045+
pub fn rotate_z(&self, angle: f32) -> PyResult<()> {
1046+
graphics_record_command(self.entity, DrawCommand::RotateZ { angle })
1047+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
1048+
}
1049+
8691050
pub fn draw_box(&self, width: f32, height: f32, depth: f32) -> PyResult<()> {
8701051
graphics_record_command(
8711052
self.entity,

0 commit comments

Comments
 (0)