Skip to content

Commit 0c171e5

Browse files
authored
Merge pull request #3402 from XFajk/main
Implemented the Line.project
2 parents 69114b4 + eb078bc commit 0c171e5

File tree

7 files changed

+138
-0
lines changed

7 files changed

+138
-0
lines changed

buildconfig/stubs/pygame/geometry.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,4 @@ class Line:
197197
def scale_ip(self, factor_and_origin: Point, /) -> None: ...
198198
def flip_ab(self) -> Line: ...
199199
def flip_ab_ip(self) -> None: ...
200+
def project(self, point: Point, clamp: bool = False) -> tuple[float, float]: ...
14.2 KB
Loading
30.2 KB
Loading

docs/reST/ref/geometry.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,3 +740,30 @@
740740
.. versionadded:: 2.5.3
741741

742742
.. ## Line.flip_ab_ip ##
743+
744+
.. method:: project
745+
746+
| :sl:`projects the line onto the given line`
747+
| :sg:`project(point: tuple[float, float], clamp=False) -> tuple[float, float]`
748+
749+
This method takes in a point and one boolean keyword argument clamp. It outputs an orthogonally projected point onto the line.
750+
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.
751+
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.
752+
753+
754+
.. figure:: code_examples/project.png
755+
:alt: project method image
756+
757+
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.
758+
759+
760+
.. figure:: code_examples/project_clamp.png
761+
:alt: project clamp argument image
762+
763+
Example of what the clamp argument changes. If it is `True`, the point is bounded between the line segment ends.
764+
765+
WARNING: If the line has no length (i.e. the start and end points are the same) then the returned point of this function will be the same point as both ends of the line.
766+
767+
.. versionadded:: 2.5.6
768+
769+
.. ## Line.project ##

src_c/doc/geometry_doc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@
4545
#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"
4646
#define DOC_LINE_FLIPAB "flip_ab() -> Line\nflips the line a and b points"
4747
#define DOC_LINE_FLIPABIP "flip_ab_ip() -> None\nflips the line a and b points, in place"
48+
#define DOC_LINE_PROJECT "project(point: tuple[float, float], clamp=False) -> tuple[float, float]\nprojects the line onto the given line"

src_c/line.c

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,80 @@ pg_line_scale_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
219219
Py_RETURN_NONE;
220220
}
221221

222+
static PyObject *
223+
_line_project_helper(pgLineBase *line, double *point, int clamp)
224+
{
225+
// this is a vector that goes from one point of the line to another
226+
double line_vector[2] = {line->bx - line->ax, line->by - line->ay};
227+
double squared_line_length =
228+
line_vector[0] * line_vector[0] + line_vector[1] * line_vector[1];
229+
230+
if (squared_line_length == 0.0) {
231+
double projected_point[2];
232+
projected_point[0] = line->ax;
233+
projected_point[1] = line->ay;
234+
return pg_tuple_couple_from_values_double(projected_point[0],
235+
projected_point[1]);
236+
}
237+
238+
// this is a vector that goes from the start of the line to the point we
239+
// are projecting onto the line
240+
double vector_from_line_start_to_point[2] = {point[0] - line->ax,
241+
point[1] - line->ay};
242+
243+
double dot_product =
244+
(vector_from_line_start_to_point[0] * line_vector[0] +
245+
vector_from_line_start_to_point[1] * line_vector[1]) /
246+
squared_line_length;
247+
248+
double projection[2] = {dot_product * line_vector[0],
249+
dot_product * line_vector[1]};
250+
251+
if (clamp) {
252+
if (dot_product < 0) {
253+
projection[0] = 0;
254+
projection[1] = 0;
255+
}
256+
else if (projection[0] * projection[0] +
257+
projection[1] * projection[1] >
258+
squared_line_length) {
259+
projection[0] = line_vector[0];
260+
projection[1] = line_vector[1];
261+
}
262+
}
263+
264+
double projected_point[2] = {line->ax + projection[0],
265+
line->ay + projection[1]};
266+
267+
return pg_tuple_couple_from_values_double(projected_point[0],
268+
projected_point[1]);
269+
}
270+
271+
static PyObject *
272+
pg_line_project(pgLineObject *self, PyObject *args, PyObject *kwnames)
273+
{
274+
double point[2] = {0.f, 0.f};
275+
int clamp = 0;
276+
277+
PyObject *point_obj = NULL;
278+
279+
static char *kwlist[] = {"point", "clamp", NULL};
280+
281+
if (!PyArg_ParseTupleAndKeywords(args, kwnames, "O|p:project", kwlist,
282+
&point_obj, &clamp)) {
283+
return RAISE(
284+
PyExc_TypeError,
285+
"project requires a sequence(point) and an optional clamp flag");
286+
}
287+
288+
if (!pg_TwoDoublesFromObj(point_obj, &point[0], &point[1])) {
289+
return RAISE(PyExc_TypeError,
290+
"project requires a sequence of two numbers");
291+
}
292+
293+
return _line_project_helper(&pgLine_AsLine(self), point, clamp);
294+
}
295+
222296
static struct PyMethodDef pg_line_methods[] = {
223297
{"__copy__", (PyCFunction)pg_line_copy, METH_NOARGS, DOC_LINE_COPY},
224298
{"copy", (PyCFunction)pg_line_copy, METH_NOARGS, DOC_LINE_COPY},
@@ -231,6 +305,8 @@ static struct PyMethodDef pg_line_methods[] = {
231305
{"scale", (PyCFunction)pg_line_scale, METH_FASTCALL, DOC_LINE_SCALE},
232306
{"scale_ip", (PyCFunction)pg_line_scale_ip, METH_FASTCALL,
233307
DOC_LINE_SCALEIP},
308+
{"project", (PyCFunction)pg_line_project, METH_VARARGS | METH_KEYWORDS,
309+
DOC_LINE_PROJECT},
234310
{NULL, NULL, 0, NULL}};
235311

236312
static PyObject *

test/geometry_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,6 +2201,39 @@ def test_meth_update(self):
22012201
with self.assertRaises(TypeError):
22022202
line.update(1, 2, 3)
22032203

2204+
def test_meth_project(self):
2205+
line = Line(0, 0, 100, 100)
2206+
test_point1 = (25, 75)
2207+
test_clamp_point1 = (100, 300)
2208+
test_clamp_point2 = (-50, -150)
2209+
test_clamp_point3 = (-200, -200)
2210+
2211+
bad_line = Line(0, 0, 0, 0)
2212+
test_bad_line_point = (10, 10)
2213+
2214+
projected_point = line.project(test_point1)
2215+
self.assertEqual(math.ceil(projected_point[0]), 50)
2216+
self.assertEqual(math.ceil(projected_point[1]), 50)
2217+
2218+
projected_point = line.project(test_clamp_point1, clamp=True)
2219+
self.assertEqual(math.ceil(projected_point[0]), 100)
2220+
self.assertEqual(math.ceil(projected_point[1]), 100)
2221+
2222+
projected_point = line.project(test_clamp_point2, clamp=True)
2223+
self.assertEqual(math.ceil(projected_point[0]), 0)
2224+
self.assertEqual(math.ceil(projected_point[1]), 0)
2225+
2226+
projected_point = line.project(test_clamp_point3, clamp=True)
2227+
self.assertEqual(math.ceil(projected_point[0]), 0)
2228+
self.assertEqual(math.ceil(projected_point[1]), 0)
2229+
2230+
projected_point = bad_line.project(test_bad_line_point, clamp=True)
2231+
self.assertEqual(math.ceil(projected_point[0]), 0)
2232+
self.assertEqual(math.ceil(projected_point[1]), 0)
2233+
2234+
projected_point = bad_line.project(test_bad_line_point)
2235+
self.assertEqual(math.ceil(projected_point[0]), 0)
2236+
22042237
def test__str__(self):
22052238
"""Checks whether the __str__ method works correctly."""
22062239
l_str = "Line((10.1, 10.2), (4.3, 56.4))"

0 commit comments

Comments
 (0)