Skip to content

Implemented the Line.project #3402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .clangd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CompileFlags:
CompilationDatabase: ./.mesonpy-build/
1 change: 1 addition & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]: ...
Binary file added docs/reST/ref/code_examples/project.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/reST/ref/code_examples/project_clamp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ##
1 change: 1 addition & 0 deletions src_c/doc/geometry_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
82 changes: 82 additions & 0 deletions src_c/line.c
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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 *
Expand Down
34 changes: 34 additions & 0 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))"
Expand Down
Loading