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 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
22ccf84
implemented the Line.project method with all the tests and docs
XFajk Apr 14, 2025
e2001dd
added the doc string to the method in C
XFajk Apr 14, 2025
193071a
optimized the Line.project method made it METH_FASTCALL and removed s…
XFajk Apr 15, 2025
ea76d39
made the docs more clear, improved the images
XFajk Apr 15, 2025
44ab4fc
Update docs/reST/ref/geometry.rst
XFajk Apr 16, 2025
cd9c648
Update docs/reST/ref/geometry.rst
XFajk Apr 16, 2025
896f7b2
Update docs/reST/ref/geometry.rst
XFajk Apr 16, 2025
ed8af87
added "/" to the stub file to indicate that the first arg is position…
XFajk Apr 15, 2025
bfe2698
added one more test case fixed the stub
XFajk Apr 16, 2025
a291329
made a small change to the stub
XFajk Apr 16, 2025
317719e
fixed a bug in the Line.project method
XFajk Apr 16, 2025
e6c1f8c
remade the method to be VARARGS insted of FASTCALL
XFajk Apr 29, 2025
383741f
added proper error handling and and reflected that in the docs and tests
XFajk Apr 29, 2025
f7178f6
removed one mistake made
XFajk Apr 29, 2025
e89deaa
Update line.c
XFajk Apr 29, 2025
d06b383
Update line.c
XFajk Apr 29, 2025
81dc704
Delete .clangd unimportant file
XFajk Jun 19, 2025
70dc960
Update buildconfig/stubs/pygame/geometry.pyi
XFajk Jun 20, 2025
82c43c9
Update geometry.rst
XFajk Jun 20, 2025
32ae369
Update src_c/line.c
XFajk Jun 20, 2025
1a8d9f6
Update src_c/line.c
XFajk Jun 20, 2025
2401414
Update src_c/line.c
XFajk Jun 20, 2025
4194dce
Fix typo in variable name for squared line length in _line_project_he…
XFajk Jun 20, 2025
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
3 changes: 3 additions & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,6 @@ 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: tuple[float, float], clamp: bool = False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I wasn't clear. Only the return type should be changed to tuple[float, float]; the argument should stay Point.

The reason is that while the input argument should be as broad and abstract of a type as possible, the return type needs to be more concrete and narrow to allow easy usage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now when talking about this, not related to this PR, but maybe we should rename the typing.(Int)Point to typing.(Int)PointLike, just to be the same as the rest of typing module, and a little bit more clear to avoid stuff like that in the future. On the other hand, it is hard to do that now since the typing module is exposed in pygame API, so maybe in pg3

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would normally agree with increasing naming consistency.
However, Point is not necessarily like the other types in that it is not a union of similar, acceptable types. It is also not a raw protocol like SequenceLike.
The main reason of course is verbosity; originally, CoordinateLike would be too long which might be the original reason, but PointLike is still long too.

) -> 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.
24 changes: 24 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,27 @@
.. 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.


.. figure:: code_examples/project.png
:alt: project method image

Example of how it projects the point on to the line purplish 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 do_clamp=True image

Example of what the clamp argument changes if it is true it stays on the line segment

.. versionadded:: 2.5.4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would need to be updated to 2.5.6 after we have consensus on this PR


.. ## 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"
89 changes: 89 additions & 0 deletions src_c/line.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,66 @@ _line_scale_helper(pgLineBase *line, double factor, double origin)
return 1;
}

void
_normalize_vector(double *vector)
{
double length = sqrt(vector[0] * vector[0] + vector[1] * vector[1]);
// check to see if the vector is zero
if (length == 0) {
vector[0] = 0;
vector[1] = 0;
}
else {
vector[0] /= length;
vector[1] /= length;
}
}

double
_length_of_vector(double *vector)
{
return sqrt(vector[0] * vector[0] + vector[1] * vector[1]);
}

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};

// 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 (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];
}
else if (dot_product < 0) {
projection[0] = 0;
projection[1] = 0;
}
}

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_scale(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
{
Expand Down Expand Up @@ -219,6 +279,33 @@ pg_line_scale_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
Py_RETURN_NONE;
}

static PyObject *
pg_line_project(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs,
PyObject *kwnames)
{
double point[2] = {0.f, 0.f};
int clamp = 0;

if (nargs >= 1) {
if (!pg_TwoDoublesFromObj(args[0], &point[0], &point[1])) {
return RAISE(PyExc_TypeError,
"project requires a sequence of two numbers");
}
}

if (kwnames != NULL) {
clamp = PyObject_IsTrue(args[nargs]);
}

PyObject *projected_point;
if (!(projected_point =
_line_project_helper(&pgLine_AsLine(self), point, clamp))) {
return NULL;
}

return projected_point;
}

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 +318,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_FASTCALL | METH_KEYWORDS,
DOC_LINE_PROJECT},
{NULL, NULL, 0, NULL}};

static PyObject *
Expand Down
18 changes: 18 additions & 0 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2201,6 +2201,24 @@ 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)

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, do_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, do_clamp=True)
self.assertEqual(math.ceil(projected_point[0]), 0)
self.assertEqual(math.ceil(projected_point[1]), 0)

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