diff --git a/.clangd b/.clangd new file mode 100644 index 0000000000..e4a68e17b5 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + CompilationDatabase: ./.mesonpy-build/ diff --git a/buildconfig/stubs/pygame/geometry.pyi b/buildconfig/stubs/pygame/geometry.pyi index 43724816ec..985d6050cb 100644 --- a/buildconfig/stubs/pygame/geometry.pyi +++ b/buildconfig/stubs/pygame/geometry.pyi @@ -197,3 +197,4 @@ class Line: def scale_ip(self, factor_and_origin: Point, /) -> None: ... def flip_ab(self) -> Line: ... def flip_ab_ip(self) -> None: ... + def project(self, point: Point, /, clamp: bool = False) -> tuple[float, float]: ... diff --git a/docs/reST/ref/code_examples/project.png b/docs/reST/ref/code_examples/project.png new file mode 100644 index 0000000000..3177bf05f6 Binary files /dev/null and b/docs/reST/ref/code_examples/project.png differ diff --git a/docs/reST/ref/code_examples/project_clamp.png b/docs/reST/ref/code_examples/project_clamp.png new file mode 100644 index 0000000000..e3447bc994 Binary files /dev/null and b/docs/reST/ref/code_examples/project_clamp.png differ diff --git a/docs/reST/ref/geometry.rst b/docs/reST/ref/geometry.rst index 12ffc5ec9f..032f01dcdb 100644 --- a/docs/reST/ref/geometry.rst +++ b/docs/reST/ref/geometry.rst @@ -740,3 +740,30 @@ .. versionadded:: 2.5.3 .. ## Line.flip_ab_ip ## + + .. method:: project + + | :sl:`projects the line onto the given line` + | :sg:`project(point: tuple[float, float], clamp=False) -> tuple[float, float]` + + This method takes in a point and one boolean keyword argument clamp. It outputs an orthogonally projected point onto the line. + If clamp is `True` it makes sure that the outputted point will be on the line segment (which might not be orthogonal), and if it is `False` (the default) then any point on the infinitely extended line may be outputted. + This method can be used to find the closest point on a line to the given point. The output is the unique point on the line or line segment that is the smallest distance away from the given point. + + + .. figure:: code_examples/project.png + :alt: project method image + + Example of how it projects the point onto the line. The red point is the point we want to project and the blue point is what you would get as a result. + + + .. figure:: code_examples/project_clamp.png + :alt: project clamp argument image + + Example of what the clamp argument changes. If it is `True`, the point is bounded between the line segment ends. + + WARNING: This method has to have some length or the clamp parameter must be true for it to work and not throw a `ValueError` + + .. versionadded:: 2.5.4 + + .. ## Line.project ## diff --git a/src_c/doc/geometry_doc.h b/src_c/doc/geometry_doc.h index 7873df226f..6c061840d3 100644 --- a/src_c/doc/geometry_doc.h +++ b/src_c/doc/geometry_doc.h @@ -45,3 +45,4 @@ #define DOC_LINE_SCALEIP "scale_ip(factor, origin) -> None\nscale_ip(factor_and_origin) -> None\nscales the line by the given factor from the given origin in place" #define DOC_LINE_FLIPAB "flip_ab() -> Line\nflips the line a and b points" #define DOC_LINE_FLIPABIP "flip_ab_ip() -> None\nflips the line a and b points, in place" +#define DOC_LINE_PROJECT "project(point: tuple[float, float], clamp=False) -> tuple[float, float]\nprojects the line onto the given line" diff --git a/src_c/line.c b/src_c/line.c index ad25ed4866..f03c5874b6 100644 --- a/src_c/line.c +++ b/src_c/line.c @@ -219,6 +219,86 @@ pg_line_scale_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) Py_RETURN_NONE; } +static PyObject * +_line_project_helper(pgLineBase *line, double *point, int clamp) +{ + // this is a vector that goes from one point of the line to another + double line_vector[2] = {line->bx - line->ax, line->by - line->ay}; + double squred_line_length = + line_vector[0] * line_vector[0] + line_vector[1] * line_vector[1]; + + if (squred_line_length == 0.0 && clamp) { + double projected_point[2]; + projected_point[0] = line->ax; + projected_point[1] = line->ay; + return pg_tuple_couple_from_values_double(projected_point[0], + projected_point[1]); + } + else if (squred_line_length == 0.0) { + return RAISE(PyExc_ValueError, + "The Line has to have some length or this method has to " + "be clamped to work"); + } + + // this is a vector that goes from the start of the line to the point we + // are projecting onto the line + double vector_from_line_start_to_point[2] = {point[0] - line->ax, + point[1] - line->ay}; + + double dot_product = + (vector_from_line_start_to_point[0] * line_vector[0] + + vector_from_line_start_to_point[1] * line_vector[1]) / + (line_vector[0] * line_vector[0] + line_vector[1] * line_vector[1]); + + double projection[2] = {dot_product * line_vector[0], + dot_product * line_vector[1]}; + + if (clamp) { + if (dot_product < 0) { + projection[0] = 0; + projection[1] = 0; + } + else if (projection[0] * projection[0] + + projection[1] * projection[1] > + line_vector[0] * line_vector[0] + + line_vector[1] * line_vector[1]) { + projection[0] = line_vector[0]; + projection[1] = line_vector[1]; + } + } + + double projected_point[2] = {line->ax + projection[0], + line->ay + projection[1]}; + + return pg_tuple_couple_from_values_double(projected_point[0], + projected_point[1]); +} + +static PyObject * +pg_line_project(pgLineObject *self, PyObject *args, PyObject *kwnames) +{ + double point[2] = {0.f, 0.f}; + int clamp = 0; + + PyObject *point_obj = NULL; + + static char *kwlist[] = {"point", "clamp", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwnames, "O|p:project", kwlist, + &point_obj, &clamp)) { + return RAISE( + PyExc_TypeError, + "project requires a sequence(point) and an optional clamp flag"); + } + + if (!pg_TwoDoublesFromObj(point_obj, &point[0], &point[1])) { + return RAISE(PyExc_TypeError, + "project requires a sequence of two numbers"); + } + + return _line_project_helper(&pgLine_AsLine(self), point, clamp); +} + static struct PyMethodDef pg_line_methods[] = { {"__copy__", (PyCFunction)pg_line_copy, METH_NOARGS, DOC_LINE_COPY}, {"copy", (PyCFunction)pg_line_copy, METH_NOARGS, DOC_LINE_COPY}, @@ -231,6 +311,8 @@ static struct PyMethodDef pg_line_methods[] = { {"scale", (PyCFunction)pg_line_scale, METH_FASTCALL, DOC_LINE_SCALE}, {"scale_ip", (PyCFunction)pg_line_scale_ip, METH_FASTCALL, DOC_LINE_SCALEIP}, + {"project", (PyCFunction)pg_line_project, METH_VARARGS | METH_KEYWORDS, + DOC_LINE_PROJECT}, {NULL, NULL, 0, NULL}}; static PyObject * diff --git a/test/geometry_test.py b/test/geometry_test.py index c4d48e0d6a..8d9ab89b08 100644 --- a/test/geometry_test.py +++ b/test/geometry_test.py @@ -2201,6 +2201,40 @@ def test_meth_update(self): with self.assertRaises(TypeError): line.update(1, 2, 3) + def test_meth_project(self): + line = Line(0, 0, 100, 100) + test_point1 = (25, 75) + test_clamp_point1 = (100, 300) + test_clamp_point2 = (-50, -150) + test_clamp_point3 = (-200, -200) + + bad_line = Line(0, 0, 0, 0) + test_bad_line_point = (10, 10) + + projected_point = line.project(test_point1) + self.assertEqual(math.ceil(projected_point[0]), 50) + self.assertEqual(math.ceil(projected_point[1]), 50) + + projected_point = line.project(test_clamp_point1, clamp=True) + self.assertEqual(math.ceil(projected_point[0]), 100) + self.assertEqual(math.ceil(projected_point[1]), 100) + + projected_point = line.project(test_clamp_point2, clamp=True) + self.assertEqual(math.ceil(projected_point[0]), 0) + self.assertEqual(math.ceil(projected_point[1]), 0) + + projected_point = line.project(test_clamp_point3, clamp=True) + self.assertEqual(math.ceil(projected_point[0]), 0) + self.assertEqual(math.ceil(projected_point[1]), 0) + + projected_point = bad_line.project(test_bad_line_point, clamp=True) + self.assertEqual(math.ceil(projected_point[0]), 0) + self.assertEqual(math.ceil(projected_point[1]), 0) + + # testing if the method fails when it should + with self.assertRaises(ValueError): + bad_line.project(test_bad_line_point) + def test__str__(self): """Checks whether the __str__ method works correctly.""" l_str = "Line((10.1, 10.2), (4.3, 56.4))"