diff --git a/buildconfig/stubs/pygame/_render.pyi b/buildconfig/stubs/pygame/_render.pyi index 2a76653dec..bb4d87f645 100644 --- a/buildconfig/stubs/pygame/_render.pyi +++ b/buildconfig/stubs/pygame/_render.pyi @@ -40,6 +40,8 @@ class Renderer: def fill_rect(self, rect: RectLike) -> None: ... def fill_triangle(self, p1: Point, p2: Point, p3: Point) -> None: ... def get_viewport(self) -> Rect: ... + def logical_to_window(self, point: Point) -> tuple[float, float]: ... + def window_to_logical(self, point: Point) -> tuple[float, float]: ... def present(self) -> None: ... def set_viewport(self, area: Optional[RectLike]) -> None: ... def to_surface( diff --git a/buildconfig/stubs/pygame/_sdl2/video.pyi b/buildconfig/stubs/pygame/_sdl2/video.pyi index e9ce224759..28c74caa18 100644 --- a/buildconfig/stubs/pygame/_sdl2/video.pyi +++ b/buildconfig/stubs/pygame/_sdl2/video.pyi @@ -146,6 +146,8 @@ class Renderer: def get_viewport(self) -> Rect: ... def set_viewport(self, area: Optional[RectLike]) -> None: ... logical_size: Iterable[int] + def logical_to_window(self, point: Point) -> tuple[float, float]: ... + def window_to_logical(self, point: Point) -> tuple[float, float]: ... scale: Iterable[float] target: Optional[Texture] def blit( diff --git a/docs/reST/ref/sdl2_video.rst b/docs/reST/ref/sdl2_video.rst index 99ce737c7f..ea25eeb865 100644 --- a/docs/reST/ref/sdl2_video.rst +++ b/docs/reST/ref/sdl2_video.rst @@ -424,6 +424,25 @@ drawing area on the target, or ``None`` to use the entire area of the current rendering target. + .. method:: logical_to_window + + | :sl:`Translates logical coordinates to window coordinates` + | :sg:`logical_to_window(point) -> (float, float)` + + :param point: The coordinates in logical space. + + .. versionadded:: 2.5.6 + + + .. method:: window_to_logical + + | :sl:`Translates window coordinates to logical coordinates` + | :sg:`window_to_logical(point) -> (float, float)` + + :param point: The coordinates in window space. + + .. versionadded:: 2.5.6 + .. method:: blit | :sl:`Draw textures using a Surface-like API` diff --git a/src_c/cython/pygame/_sdl2/video.pxd b/src_c/cython/pygame/_sdl2/video.pxd index 1d87ba43b2..1fe2fc6354 100644 --- a/src_c/cython/pygame/_sdl2/video.pxd +++ b/src_c/cython/pygame/_sdl2/video.pxd @@ -369,6 +369,8 @@ cdef extern from "SDL.h" nogil: # https://wiki.libsdl.org/SDL_RenderSetLogicalSize # https://wiki.libsdl.org/SDL_RenderGetLogicalSize # https://wiki.libsdl.org/SDL_RenderGetIntegerScale + # https://wiki.libsdl.org/SDL2/SDL_RenderLogicalToWindow + # https://wiki.libsdl.org/SDL2/SDL_RenderWindowToLogical int SDL_RenderSetScale(SDL_Renderer* renderer, float scaleX, float scaleY) @@ -382,6 +384,20 @@ cdef extern from "SDL.h" nogil: int* w, int* h) int SDL_RenderGetIntegerScale(SDL_Renderer* renderer) + # Note: Must be changed to SDL_RenderCoordinatesToWindow for SDL3 + # https://wiki.libsdl.org/SDL3/SDL_RenderCoordinatesToWindow + void SDL_RenderLogicalToWindow(SDL_Renderer* renderer, + float lx, + float ly, + int *wx, + int *wy); + # Note: Must be changed to SDL_RenderCoordinatesFromWindow for SDL3 + # https://wiki.libsdl.org/SDL3/SDL_RenderCoordinatesFromWindow + void SDL_RenderWindowToLogical(SDL_Renderer* renderer, + int wx, + int wy, + float *lx, + float *ly); int SDL_VERSION_ATLEAST(int major, int minor, int patch) diff --git a/src_c/cython/pygame/_sdl2/video.pyx b/src_c/cython/pygame/_sdl2/video.pyx index af8ea28e1e..a0f9db9fd8 100644 --- a/src_c/cython/pygame/_sdl2/video.pyx +++ b/src_c/cython/pygame/_sdl2/video.pyx @@ -1037,6 +1037,35 @@ cdef class Renderer: if res < 0: raise error() + def logical_to_window(self, point): + """Translates logical coordinates to window coordinates + + :param point: The coordinates in logical space. + """ + cdef int wx + cdef int wy + + # Note: Must be changed to SDL_RenderCoordinatesToWindow for SDL3 + # https://wiki.libsdl.org/SDL3/SDL_RenderCoordinatesToWindow + SDL_RenderLogicalToWindow(self._renderer, point[0], point[1], &wx, &wy); + + # Return float for future compatibility with SDL3's RenderCoordinatesToWindow + return (float(wx), float(wy)) + + def window_to_logical(self, point): + """Translates window coordinates to logical coordinates + + :param point: The coordinates in window space. + """ + cdef float lx + cdef float ly + + # Note: Must be changed to SDL_RenderCoordinatesFromWindow for SDL3 + # https://wiki.libsdl.org/SDL3/SDL_RenderCoordinatesFromWindow + SDL_RenderWindowToLogical(self._renderer, point[0], point[1], &lx, &ly); + + return (lx, ly) + def draw_point(self, point): """Draw a point diff --git a/src_c/doc/sdl2_video_doc.h b/src_c/doc/sdl2_video_doc.h index 8ca553bdfc..3fd07689aa 100644 --- a/src_c/doc/sdl2_video_doc.h +++ b/src_c/doc/sdl2_video_doc.h @@ -41,6 +41,8 @@ #define DOC_SDL2_VIDEO_RENDERER_PRESENT "present() -> None\nUpdate the screen with any rendering performed since the previous call" #define DOC_SDL2_VIDEO_RENDERER_GETVIEWPORT "get_viewport() -> Rect\nGet the drawing area on the rendering target" #define DOC_SDL2_VIDEO_RENDERER_SETVIEWPORT "set_viewport(area) -> None\nSet the drawing area on the rendering target" +#define DOC_SDL2_VIDEO_RENDERER_LOGICALTOWINDOW "logical_to_window(point) -> (float, float)\nTranslates logical coordinates to window coordinates" +#define DOC_SDL2_VIDEO_RENDERER_WINDOWTOLOGICAL "window_to_logical(point) -> (float, float)\nTranslates window coordinates to logical coordinates" #define DOC_SDL2_VIDEO_RENDERER_BLIT "blit(source, dest, area=None, special_flags=0)-> Rect\nDraw textures using a Surface-like API" #define DOC_SDL2_VIDEO_RENDERER_DRAWLINE "draw_line(p1, p2) -> None\nDraw a line" #define DOC_SDL2_VIDEO_RENDERER_DRAWPOINT "draw_point(point) -> None\nDraw a point" diff --git a/src_c/render.c b/src_c/render.c index d237ced3e7..15245eaa7c 100644 --- a/src_c/render.c +++ b/src_c/render.c @@ -443,6 +443,61 @@ renderer_set_logical_size(pgRendererObject *self, PyObject *arg, void *closure) return 0; } +static PyObject * +renderer_logical_to_window(pgRendererObject *self, PyObject *args, + PyObject *kwargs) +{ + float lx, ly; + PyObject *point; + + static char *keywords[] = {"point", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &point)) { + return NULL; + } + if (!pg_TwoFloatsFromObj(point, &lx, &ly)) { + return RAISE(PyExc_TypeError, + "point must be a sequence of two numbers"); + } + +#if SDL_VERSION_ATLEAST(3, 0, 0) + float wx, wy; + SDL_RenderCoordinatesToWindow(self->renderer, lx, ly, &wx, &wy); + + return pg_tuple_couple_from_values_double(wx, wy); +#else + int wx, wy; + SDL_RenderLogicalToWindow(self->renderer, lx, ly, &wx, &wy); + + return pg_tuple_couple_from_values_double((float)wx, (float)wy); +#endif +} + +static PyObject * +renderer_window_to_logical(pgRendererObject *self, PyObject *args, + PyObject *kwargs) +{ + float lx, ly; + float wx, wy; + PyObject *point; + + static char *keywords[] = {"point", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &point)) { + return NULL; + } + if (!pg_TwoFloatsFromObj(point, &wx, &wy)) { + return RAISE(PyExc_TypeError, + "point must be a sequence of two numbers"); + } + +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_RenderCoordinatesFromWindow(self->renderer, wx, wy, &lx, &ly); +#else + SDL_RenderWindowToLogical(self->renderer, (int)wx, (int)wy, &lx, &ly); +#endif + + return pg_tuple_couple_from_values_double(lx, ly); +} + static PyObject * renderer_get_scale(pgRendererObject *self, void *closure) { @@ -582,6 +637,10 @@ static PyMethodDef renderer_methods[] = { METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_SETVIEWPORT}, {"get_viewport", (PyCFunction)renderer_get_viewport, METH_NOARGS, DOC_SDL2_VIDEO_RENDERER_GETVIEWPORT}, + {"logical_to_window", (PyCFunction)renderer_logical_to_window, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_LOGICALTOWINDOW}, + {"window_to_logical", (PyCFunction)renderer_window_to_logical, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_WINDOWTOLOGICAL}, {"compose_custom_blend_mode", (PyCFunction)renderer_compose_custom_blend_mode, METH_VARARGS | METH_KEYWORDS | METH_CLASS, diff --git a/test/render_test.py b/test/render_test.py index b04a635ddd..1e527d8b6b 100644 --- a/test/render_test.py +++ b/test/render_test.py @@ -166,6 +166,15 @@ def test_logical_size(self): self.renderer.logical_size = (10, 10) self.assertEqual(self.renderer.logical_size, (10, 10)) + def test_logical_window_mapping(self): + self.renderer.logical_size = (10, 10) + self.assertEqual(self.renderer.logical_to_window((10, 10)), (100, 100)) + self.assertEqual(self.renderer.window_to_logical((100, 100)), (10, 10)) + with self.assertRaises(TypeError): + self.renderer.logical_to_window(42, 42) + with self.assertRaises(TypeError): + self.renderer.window_to_logical(42, 42) + def test_scale(self): self.assertEqual(self.renderer.scale, (1.0, 1.0)) self.renderer.scale = (0.5, 2) diff --git a/test/video_test.py b/test/video_test.py index f6876bc56c..9787b4e245 100644 --- a/test/video_test.py +++ b/test/video_test.py @@ -24,6 +24,18 @@ def test_renderer_set_viewport(self): renderer.set_viewport(rect) self.assertEqual(renderer.get_viewport(), (0, 0, 1920, 1080)) + def test_logical_window_mapping(self): + window = video.Window(title=self.default_caption, size=(100, 100)) + renderer = video.Renderer(window=window) + renderer.logical_size = (10, 10) + + self.assertEqual(renderer.logical_to_window((10, 10)), (100, 100)) + self.assertEqual(renderer.window_to_logical((100, 100)), (10, 10)) + with self.assertRaises(TypeError): + renderer.logical_to_window(42, 42) + with self.assertRaises(TypeError): + renderer.window_to_logical(42, 42) + @unittest.skipIf(IS_PYPY, "PyPy doesn't have sys.getrefcount") def test_renderer_to_surface_refcount(self): """Make sure to_surface doesn't leak memory due to reference counting."""