From 19a17c952b7ccd2b51293867f0a90bc9b1c25251 Mon Sep 17 00:00:00 2001 From: Lody <69472620+bilhox@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:47:16 +0200 Subject: [PATCH] ``invlerp`` and ``remap`` implementation (#2654) * invlerp & remap impl --------- Co-authored-by: Dan Lawrence Co-authored-by: Ankith --- buildconfig/stubs/pygame/math.pyi | 2 + docs/reST/ref/math.rst | 53 ++++++++++++++++ src_c/doc/math_doc.h | 2 + src_c/math.c | 80 +++++++++++++++++++++++ test/math_test.py | 101 ++++++++++++++++++++++++++++++ 5 files changed, 238 insertions(+) diff --git a/buildconfig/stubs/pygame/math.pyi b/buildconfig/stubs/pygame/math.pyi index ebb99fb232..82ec91c7a0 100644 --- a/buildconfig/stubs/pygame/math.pyi +++ b/buildconfig/stubs/pygame/math.pyi @@ -334,6 +334,8 @@ class Vector3(_GenericVector): def lerp(a: float, b: float, weight: float, do_clamp: bool = True, /) -> float: ... +def invlerp(a: float, b: float, weight: float, /) -> float: ... +def remap(i_min: float, i_max: float, o_min: float, o_max: float, value: float, /) -> float: ... def smoothstep(a: float, b: float, weight: float, /) -> float: ... diff --git a/docs/reST/ref/math.rst b/docs/reST/ref/math.rst index 9ab1d6cd9d..93cb998bbc 100644 --- a/docs/reST/ref/math.rst +++ b/docs/reST/ref/math.rst @@ -79,6 +79,37 @@ Multiple coordinates can be set using slices or swizzling .. ## math.lerp ## +.. function:: invlerp + + | :sl:`returns value inverse interpolated between a and b` + | :sg:`invlerp(a, b, value, /) -> float` + + Returns a number which is an inverse interpolation between ``a`` + and ``b``. The third parameter ``value`` is the result of the linear interpolation + between a and b with a certain coefficient. In other words, this coefficient + will be the result of this function. + If ``b - a`` is equal to 0, it raises a ``ZeroDivisionError``. + + The formula is: + + ``(v - a)/(b - a)``. + + This is an example explaining what is above : + + .. code-block:: python + + > a = 10 + > b = 20 + > pygame.math.invlerp(10, 20, 11.5) + > 0.15 + > pygame.math.lerp(10, 20, 0.15) + > 11.5 + + + .. versionadded:: 2.5.0 + + .. ## math.invlerp ## + .. function:: smoothstep | :sl:`returns value smoothly interpolated between a and b.` @@ -102,6 +133,28 @@ Multiple coordinates can be set using slices or swizzling .. ## math.smoothstep ## +.. function:: remap + + | :sl:`remaps value from i_range to o_range` + | :sg:`remap(i_min, i_max, o_min, o_max, value, /) -> float` + + Returns a number which is the value remapped from ``i_range`` to + ``o_range``. + If ``i_max - i_min`` is equal to 0, it raises a ``ZeroDivisionError``. + + Example: + + .. code-block:: python + + > value = 50 + > pygame.math.remap(0, 100, 0, 200, value) + > 100.0 + + + .. versionadded:: 2.5.0 + + .. ## math.remap ## + .. class:: Vector2 | :sl:`a 2-Dimensional Vector` diff --git a/src_c/doc/math_doc.h b/src_c/doc/math_doc.h index ea325c6291..f5607ce918 100644 --- a/src_c/doc/math_doc.h +++ b/src_c/doc/math_doc.h @@ -2,7 +2,9 @@ #define DOC_MATH "pygame module for vector classes" #define DOC_MATH_CLAMP "clamp(value, min, max, /) -> float\nreturns value clamped to min and max." #define DOC_MATH_LERP "lerp(a, b, value, do_clamp=True, /) -> float\nreturns value linearly interpolated between a and b" +#define DOC_MATH_INVLERP "invlerp(a, b, value, /) -> float\nreturns value inverse interpolated between a and b" #define DOC_MATH_SMOOTHSTEP "smoothstep(a, b, value, /) -> float\nreturns value smoothly interpolated between a and b." +#define DOC_MATH_REMAP "remap(i_min, i_max, o_min, o_max, value, /) -> float\nremaps value from i_range to o_range" #define DOC_MATH_VECTOR2 "Vector2() -> Vector2(0, 0)\nVector2(int) -> Vector2\nVector2(float) -> Vector2\nVector2(Vector2) -> Vector2\nVector2(x, y) -> Vector2\nVector2((x, y)) -> Vector2\na 2-Dimensional Vector" #define DOC_MATH_VECTOR2_DOT "dot(Vector2, /) -> float\ncalculates the dot- or scalar-product with the other vector" #define DOC_MATH_VECTOR2_CROSS "cross(Vector2, /) -> float\ncalculates the cross- or vector-product" diff --git a/src_c/math.c b/src_c/math.c index d8df63fc1f..8ed8ccae5e 100644 --- a/src_c/math.c +++ b/src_c/math.c @@ -4192,6 +4192,18 @@ vector_elementwise(pgVector *vec, PyObject *_null) return (PyObject *)proxy; } +inline double +lerp(double a, double b, double v) +{ + return a + (b - a) * v; +} + +inline double +invlerp(double a, double b, double v) +{ + return (v - a) / (b - a); +} + static PyObject * math_clamp(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { @@ -4233,6 +4245,72 @@ math_clamp(PyObject *self, PyObject *const *args, Py_ssize_t nargs) return value; } +#define RAISE_ARG_TYPE_ERROR(var) \ + if (PyErr_Occurred()) { \ + return RAISE(PyExc_TypeError, \ + "The argument '" var "' must be a real number"); \ + } + +static PyObject * +math_invlerp(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (nargs != 3) + return RAISE(PyExc_TypeError, + "invlerp requires exactly 3 numeric arguments"); + + double a = PyFloat_AsDouble(args[0]); + RAISE_ARG_TYPE_ERROR("a") + double b = PyFloat_AsDouble(args[1]); + RAISE_ARG_TYPE_ERROR("b") + double t = PyFloat_AsDouble(args[2]); + RAISE_ARG_TYPE_ERROR("value") + + if (PyErr_Occurred()) + return RAISE(PyExc_ValueError, + "invalid argument values passed to invlerp, numbers " + "might be too small or too big"); + + if (b - a == 0) + return RAISE(PyExc_ValueError, + "the result of b - a needs to be different from zero"); + + return PyFloat_FromDouble(invlerp(a, b, t)); +} + +# + +static PyObject * +math_remap(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (nargs != 5) + return RAISE(PyExc_TypeError, + "remap requires exactly 5 numeric arguments"); + + PyObject *i_min = args[0]; + PyObject *i_max = args[1]; + PyObject *o_min = args[2]; + PyObject *o_max = args[3]; + PyObject *value = args[4]; + + double v = PyFloat_AsDouble(value); + RAISE_ARG_TYPE_ERROR("value") + double a = PyFloat_AsDouble(i_min); + RAISE_ARG_TYPE_ERROR("i_min") + double b = PyFloat_AsDouble(i_max); + RAISE_ARG_TYPE_ERROR("i_max") + double c = PyFloat_AsDouble(o_min); + RAISE_ARG_TYPE_ERROR("o_min") + double d = PyFloat_AsDouble(o_max); + RAISE_ARG_TYPE_ERROR("o_max") + + if (b - a == 0) + return RAISE( + PyExc_ValueError, + "the result of i_max - i_min needs to be different from zero"); + + return PyFloat_FromDouble(lerp(c, d, invlerp(a, b, v))); +} + static PyObject * math_lerp(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { @@ -4343,6 +4421,8 @@ math_disable_swizzling(pgVector *self, PyObject *_null) static PyMethodDef _math_methods[] = { {"clamp", (PyCFunction)math_clamp, METH_FASTCALL, DOC_MATH_CLAMP}, {"lerp", (PyCFunction)math_lerp, METH_FASTCALL, DOC_MATH_LERP}, + {"invlerp", (PyCFunction)math_invlerp, METH_FASTCALL, DOC_MATH_INVLERP}, + {"remap", (PyCFunction)math_remap, METH_FASTCALL, DOC_MATH_REMAP}, {"smoothstep", (PyCFunction)math_smoothstep, METH_FASTCALL, DOC_MATH_SMOOTHSTEP}, {"enable_swizzling", (PyCFunction)math_enable_swizzling, METH_NOARGS, diff --git a/test/math_test.py b/test/math_test.py index cbfa81c816..bcc2c73830 100644 --- a/test/math_test.py +++ b/test/math_test.py @@ -86,6 +86,107 @@ def test_lerp(self): b = 2 pygame.math.lerp(a, b, Vector2(0, 0)) + def test_invlerp(self): + a = 0.0 + b = 10.0 + self.assertEqual(pygame.math.invlerp(a, b, 5.0), 0.5) + + a = 0.0 + b = 10.0 + self.assertEqual(pygame.math.invlerp(a, b, 0.1), 0.01) + + a = -10.0 + b = 10.0 + self.assertEqual(pygame.math.invlerp(a, b, 0.5), 0.525) + + a = -10.0 + b = 10.0 + self.assertEqual(pygame.math.invlerp(a, b, 1.5), 0.575) + + a = 0.0 + b = 100.0 + self.assertEqual(pygame.math.invlerp(a, b, 0.25), 0.0025) + + with self.assertRaises(TypeError): + a = Vector2(0, 0) + b = Vector2(10.0, 10.0) + pygame.math.invlerp(a, b, 0.5) + + with self.assertRaises(TypeError): + a = 1 + b = 2 + pygame.math.invlerp(a, b, Vector2(0, 0)) + + with self.assertRaises(ValueError): + a = 5 + b = 5 + pygame.math.invlerp(a, b, 5) + + with self.assertRaises(TypeError): + a = 12**300 + b = 11**30 + pygame.math.invlerp(a, b, 1) + + def test_remap(self): + a = 0.0 + b = 10.0 + c = 0.0 + d = 100.0 + self.assertEqual(pygame.math.remap(a, b, c, d, 1.0), 10.0) + + a = 0.0 + b = 10.0 + c = 0.0 + d = 100.0 + self.assertEqual(pygame.math.remap(a, b, c, d, -1.0), -10.0) + + a = -10.0 + b = 10.0 + c = -20.0 + d = 20.0 + self.assertEqual(pygame.math.remap(a, b, c, d, 0.0), 0.0) + + a = -10.0 + b = 10.0 + c = 10.0 + d = 110.0 + self.assertEqual(pygame.math.remap(a, b, c, d, -8.0), 20.0) + + with self.assertRaises(TypeError): + a = Vector2(0, 0) + b = "fish" + c = "durk" + d = Vector2(100, 100) + pygame.math.remap(a, b, c, d, 10) + + with self.assertRaises(TypeError): + a = 1 + b = 2 + c = 10 + d = 20 + pygame.math.remap(a, b, c, d, Vector2(0, 0)) + + with self.assertRaises(ValueError): + a = 5 + b = 5 + c = 0 + d = 100 + pygame.math.remap(a, b, c, d, 10) + + with self.assertRaises(TypeError): + a = 12**300 + b = 11**30 + c = 20 + d = 30 + pygame.math.remap(a, b, c, d, 100 * 50) + + with self.assertRaises(TypeError): + a = 12j + b = 11j + c = 10j + d = 9j + pygame.math.remap(a, b, c, d, 50j) + def test_smoothstep(self): a = 0.0 b = 10.0