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 2 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
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, do_clamp: bool = False) -> Point: ...
Binary file added docs/reST/ref/code_examples/project1.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/project2.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/project3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,31 @@
.. versionadded:: 2.5.3

.. ## Line.flip_ab_ip ##

.. method:: project

| :sl:`projects the line onto the given line`
| :sg:`project(point, do_clamp=False) -> point`

Returns a new point(tuple[float, float]) that is projected onto the line like so

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

Example of what project method does if you input the green point you get back the yellow one.


.. figure:: code_examples/project2.png
:alt: project do_clamp=False image

Example of what the do_clamp argument does when it is False


.. figure:: code_examples/project1.png
:alt: project do_clamp=True image

Example of what the do_clamp argument does when it is True

.. 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, do_clamp=False) -> point\nprojects the line onto the given line"
114 changes: 114 additions & 0 deletions src_c/line.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,70 @@ _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 do_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 line_length = _length_of_vector(line_vector);

// this is a unit vector that points in the direction of the line
double normalized_line_vector[2] = {line_vector[0], line_vector[1]};
_normalize_vector(normalized_line_vector);

// 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] * normalized_line_vector[0] +
vector_from_line_start_to_point[1] * normalized_line_vector[1];

double projection[2] = {dot_product * normalized_line_vector[0],
dot_product * normalized_line_vector[1]};

if (do_clamp) {
if (dot_product > line_length) {
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]};

PyObject *projected_tuple =
Py_BuildValue("(dd)", projected_point[0], projected_point[1]);

return projected_tuple;
}

static PyObject *
pg_line_scale(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
{
Expand Down Expand Up @@ -219,6 +283,54 @@ pg_line_scale_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
Py_RETURN_NONE;
}

static PyObject *
pg_line_project(pgLineObject *self, PyObject *args, PyObject *kwnames)
{
PyObject *point_obj = NULL;
int do_clamp = 0;

static char *kwlist[] = {"point", "do_clamp", NULL};

if (!PyArg_ParseTupleAndKeywords(args, kwnames, "O|p:project", kwlist,
&point_obj, &do_clamp)) {
return RAISE(
PyExc_TypeError,
"project requires a sequence(point) and an optional clamp flag");
}

PyObject *item;
double point[2];

if (!PySequence_Check(point_obj) || PySequence_Size(point_obj) != 2) {
return RAISE(
PyExc_ValueError,
"project requires the point to be a sequence of 2 elements");
}

item = PySequence_GetItem(point_obj, 0);
point[0] = PyFloat_AsDouble(item);

PyObject *error_type;
if ((error_type = PyErr_Occurred())) {
return NULL;
}

item = PySequence_GetItem(point_obj, 1);
point[1] = PyFloat_AsDouble(item);

if ((error_type = PyErr_Occurred())) {
return NULL;
}

PyObject *projected_point;
if (!(projected_point =
_line_project_helper(&pgLine_AsLine(self), point, do_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 +343,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
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