Skip to content

Commit 22ccf84

Browse files
committed
implemented the Line.project method with all the tests and docs
1 parent 158b227 commit 22ccf84

File tree

8 files changed

+163
-0
lines changed

8 files changed

+163
-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, do_clamp: bool = False) -> Point: ...
3.39 KB
Loading
3.92 KB
Loading
4.38 KB
Loading

docs/reST/ref/geometry.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,3 +740,31 @@
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, do_clamp=False) -> point`
748+
749+
Returns a new point(tuple[float, float]) that is projected onto the line like so
750+
751+
.. figure:: code_examples/project1.png
752+
:alt: project method image
753+
754+
Example of what project method does if you input the green point you get back the yellow one.
755+
756+
757+
.. figure:: code_examples/project2.png
758+
:alt: project do_clamp=False image
759+
760+
Example of what the do_clamp argument does when it is False
761+
762+
763+
.. figure:: code_examples/project1.png
764+
:alt: project do_clamp=True image
765+
766+
Example of what the do_clamp argument does when it is True
767+
768+
.. versionadded:: 2.5.4
769+
770+
.. ## 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, do_clamp=False) -> point\nprojects the line onto the given line"

src_c/line.c

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,70 @@ _line_scale_helper(pgLineBase *line, double factor, double origin)
179179
return 1;
180180
}
181181

182+
void
183+
_normalize_vector(double *vector)
184+
{
185+
double length = sqrt(vector[0] * vector[0] + vector[1] * vector[1]);
186+
// check to see if the vector is zero
187+
if (length == 0) {
188+
vector[0] = 0;
189+
vector[1] = 0;
190+
}
191+
else {
192+
vector[0] /= length;
193+
vector[1] /= length;
194+
}
195+
}
196+
197+
double
198+
_length_of_vector(double *vector)
199+
{
200+
return sqrt(vector[0] * vector[0] + vector[1] * vector[1]);
201+
}
202+
203+
static PyObject *
204+
_line_project_helper(pgLineBase *line, double *point, int do_clamp)
205+
{
206+
// this is a vector that goes from one point of the line to another
207+
double line_vector[2] = {line->bx - line->ax, line->by - line->ay};
208+
double line_length = _length_of_vector(line_vector);
209+
210+
// this is a unit vector that points in the direction of the line
211+
double normalized_line_vector[2] = {line_vector[0], line_vector[1]};
212+
_normalize_vector(normalized_line_vector);
213+
214+
// this is a vector that goes from the start of the line to the point we
215+
// are projecting onto the line
216+
double vector_from_line_start_to_point[2] = {point[0] - line->ax,
217+
point[1] - line->ay};
218+
219+
double dot_product =
220+
vector_from_line_start_to_point[0] * normalized_line_vector[0] +
221+
vector_from_line_start_to_point[1] * normalized_line_vector[1];
222+
223+
double projection[2] = {dot_product * normalized_line_vector[0],
224+
dot_product * normalized_line_vector[1]};
225+
226+
if (do_clamp) {
227+
if (dot_product > line_length) {
228+
projection[0] = line_vector[0];
229+
projection[1] = line_vector[1];
230+
}
231+
else if (dot_product < 0) {
232+
projection[0] = 0;
233+
projection[1] = 0;
234+
}
235+
}
236+
237+
double projected_point[2] = {line->ax + projection[0],
238+
line->ay + projection[1]};
239+
240+
PyObject *projected_tuple =
241+
Py_BuildValue("(dd)", projected_point[0], projected_point[1]);
242+
243+
return projected_tuple;
244+
}
245+
182246
static PyObject *
183247
pg_line_scale(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
184248
{
@@ -219,6 +283,54 @@ pg_line_scale_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
219283
Py_RETURN_NONE;
220284
}
221285

286+
static PyObject *
287+
pg_line_project(pgLineObject *self, PyObject *args, PyObject *kwnames)
288+
{
289+
PyObject *point_obj = NULL;
290+
int do_clamp = 0;
291+
292+
static char *kwlist[] = {"point", "do_clamp", NULL};
293+
294+
if (!PyArg_ParseTupleAndKeywords(args, kwnames, "O|p:project", kwlist,
295+
&point_obj, &do_clamp)) {
296+
return RAISE(
297+
PyExc_TypeError,
298+
"project requires a sequence(point) and an optional clamp flag");
299+
}
300+
301+
PyObject *item;
302+
double point[2];
303+
304+
if (!PySequence_Check(point_obj) || PySequence_Size(point_obj) != 2) {
305+
return RAISE(
306+
PyExc_ValueError,
307+
"project requires the point to be a sequence of 2 elements");
308+
}
309+
310+
item = PySequence_GetItem(point_obj, 0);
311+
point[0] = PyFloat_AsDouble(item);
312+
313+
PyObject *error_type;
314+
if ((error_type = PyErr_Occurred())) {
315+
return NULL;
316+
}
317+
318+
item = PySequence_GetItem(point_obj, 1);
319+
point[1] = PyFloat_AsDouble(item);
320+
321+
if ((error_type = PyErr_Occurred())) {
322+
return NULL;
323+
}
324+
325+
PyObject *projected_point;
326+
if (!(projected_point =
327+
_line_project_helper(&pgLine_AsLine(self), point, do_clamp))) {
328+
return NULL;
329+
}
330+
331+
return projected_point;
332+
}
333+
222334
static struct PyMethodDef pg_line_methods[] = {
223335
{"__copy__", (PyCFunction)pg_line_copy, METH_NOARGS, DOC_LINE_COPY},
224336
{"copy", (PyCFunction)pg_line_copy, METH_NOARGS, DOC_LINE_COPY},
@@ -231,6 +343,8 @@ static struct PyMethodDef pg_line_methods[] = {
231343
{"scale", (PyCFunction)pg_line_scale, METH_FASTCALL, DOC_LINE_SCALE},
232344
{"scale_ip", (PyCFunction)pg_line_scale_ip, METH_FASTCALL,
233345
DOC_LINE_SCALEIP},
346+
{"project", (PyCFunction)pg_line_project, METH_VARARGS | METH_KEYWORDS,
347+
""},
234348
{NULL, NULL, 0, NULL}};
235349

236350
static PyObject *

test/geometry_test.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,6 +2201,25 @@ 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+
2210+
projected_point = line.project(test_point1)
2211+
self.assertEqual(math.ceil(projected_point[0]), 50)
2212+
self.assertEqual(math.ceil(projected_point[1]), 50)
2213+
2214+
projected_point = line.project(test_clamp_point1, do_clamp=True)
2215+
self.assertEqual(math.ceil(projected_point[0]), 100)
2216+
self.assertEqual(math.ceil (projected_point[1]), 100)
2217+
2218+
projected_point = line.project(test_clamp_point2, do_clamp=True)
2219+
self.assertEqual(math.ceil(projected_point[0]), 0)
2220+
self.assertEqual(math.ceil(projected_point[1]), 0)
2221+
2222+
22042223
def test__str__(self):
22052224
"""Checks whether the __str__ method works correctly."""
22062225
l_str = "Line((10.1, 10.2), (4.3, 56.4))"

0 commit comments

Comments
 (0)