@@ -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+
135195pub 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]
171255impl 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 ,
0 commit comments