Skip to content
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

Add more Line attributes #3268

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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
16 changes: 16 additions & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,22 @@ class Line:
def b(self, value: Point) -> None: ...
@property
def length(self) -> float: ...
@property
def center(self) -> tuple[float, float]: ...
@center.setter
def center(self, value: Point) -> None: ...
@property
def centerx(self) -> float: ...
@centerx.setter
def centerx(self, value: float) -> None: ...
@property
def centery(self) -> float: ...
@centery.setter
def centery(self, value: float) -> None: ...
@property
def slope(self) -> float: ...
@property
def angle(self) -> float: ...
@overload
def __init__(self, ax: float, ay: float, bx: float, by: float) -> None: ...
@overload
Expand Down
61 changes: 61 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,67 @@

.. ## Line.length ##

.. attribute:: center

| :sl:`the coordinate of the middle point of the line`
MyreMylar marked this conversation as resolved.
Show resolved Hide resolved
| :sg:`center -> (float, float)`

The center of the line. It's equivalent to `((ax + bx) / 2, (ay + by) / 2)`.
Reassigning it moves the `Line` to the new position with matching center.

.. versionadded:: 2.5.3

.. ## Line.center ##

.. attribute:: centerx

| :sl:`the x coordinate of the middle point of the line`
| :sg:`centerx -> float`

The `x` coordinate of the line's center, calculated as :math:`(ax + bx) / 2`.
Reassigning it moves the `Line` to the new position with matching centerx.

.. versionadded:: 2.5.3

.. ## Line.centerx ##

.. attribute:: centery

| :sl:`the y coordinate of the middle point of the line`
| :sg:`centery -> float`

The `y` coordinate of the line's center, calculated as :math:`(ay + by) / 2`.
Reassigning it moves the `Line` to the new position with matching centery.

.. versionadded:: 2.5.3

.. ## Line.centery ##

.. attribute:: angle

| :sl:`the angle of the line`
| :sg:`angle -> float`

The angle of the line, representing its orientation.
It's equivalent to :math:`atan2(by - ay, bx - ax)`. This attribute is read-only and
can be changed by modifying the `a` or `b` attributes.

.. versionadded:: 2.5.3

.. ## Line.angle ##

.. attribute:: slope

| :sl:`the slope of the line`
| :sg:`slope -> float`

The slope of the line. It's equivalent to :math:`(by - ay) / (bx - ax)`.
This attribute is read-only and can be changed by modifying the `a` or `b` attributes.

.. versionadded:: 2.5.3

.. ## Line.slope ##

**Line Methods**

----
Expand Down
5 changes: 5 additions & 0 deletions src_c/doc/geometry_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
#define DOC_LINE_A "a -> (float, float)\nthe first point of the line"
#define DOC_LINE_B "b -> (float, float)\nthe second point of the line"
#define DOC_LINE_LENGTH "length -> float\nthe length of the line"
#define DOC_LINE_CENTER "center -> (float, float)\nthe coordinates of the middle point of the line"
#define DOC_LINE_CENTERX "centerx -> float\nthe x coordinate of the middle point of the line"
#define DOC_LINE_CENTERY "centery -> float\nthe y coordinate of the middle point of the line"
#define DOC_LINE_ANGLE "angle -> float\nthe angle of the line"
#define DOC_LINE_SLOPE "slope -> float\nthe slope of the line"
#define DOC_LINE_COPY "copy() -> Line\ncopies the line"
#define DOC_LINE_MOVE "move((x, y)) -> Line\nmove(x, y) -> Line\nmoves the line by a given amount"
#define DOC_LINE_MOVEIP "move_ip((x, y)) -> None\nmove_ip(x, y) -> None\nmoves the line by a given amount"
Expand Down
7 changes: 7 additions & 0 deletions src_c/geometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ static PyTypeObject pgLine_Type;
#define M_PI_QUO_180 0.01745329251994329577
#endif

/* Converts radians to degrees */
static inline double
RAD_TO_DEG(double rad)
{
return rad / M_PI_QUO_180;
}

/* Converts degrees to radians */
static inline double
DEG_TO_RAD(double deg)
Expand Down
117 changes: 117 additions & 0 deletions src_c/line.c
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,115 @@ pg_line_getlength(pgLineObject *self, void *closure)
return PyFloat_FromDouble(pgLine_Length(&self->line));
}

static PyObject *
pg_line_get_center(pgLineObject *self, void *closure)
{
return pg_tuple_couple_from_values_double(
(self->line.ax + self->line.bx) / 2,
(self->line.ay + self->line.by) / 2);
}

static int
pg_line_set_center(pgLineObject *self, PyObject *value, void *closure)
{
double m_x, m_y;
DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value);
if (!pg_TwoDoublesFromObj(value, &m_x, &m_y)) {
PyErr_SetString(
PyExc_TypeError,
"Invalid center value, expected a sequence of 2 numbers");
return -1;
}

double dx = m_x - (self->line.ax + self->line.bx) / 2;
double dy = m_y - (self->line.ay + self->line.by) / 2;

self->line.ax += dx;
self->line.ay += dy;
self->line.bx += dx;
self->line.by += dy;

return 0;
}

static PyObject *
pg_line_get_centerx(pgLineObject *self, void *closure)
{
return PyFloat_FromDouble((self->line.ax + self->line.bx) / 2);
}

static int
pg_line_set_centerx(pgLineObject *self, PyObject *value, void *closure)
{
double m_x;
DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value);
if (!pg_DoubleFromObj(value, &m_x)) {
PyErr_SetString(PyExc_TypeError,
"Invalid centerx value, expected a numeric value");
return -1;
}

double dx = m_x - (self->line.ax + self->line.bx) / 2;

self->line.ax += dx;
self->line.bx += dx;

return 0;
}

static PyObject *
pg_line_get_centery(pgLineObject *self, void *closure)
{
return PyFloat_FromDouble((self->line.ay + self->line.by) / 2);
}

static int
pg_line_set_centery(pgLineObject *self, PyObject *value, void *closure)
{
double m_y;
DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value);
if (!pg_DoubleFromObj(value, &m_y)) {
PyErr_SetString(PyExc_TypeError,
"Invalid centery value, expected a numeric value");
return -1;
}

double dy = m_y - (self->line.ay + self->line.by) / 2;

self->line.ay += dy;
self->line.by += dy;

return 0;
}

static PyObject *
pg_line_getangle(pgLineObject *self, void *closure)
{
double dx = self->line.bx - self->line.ax;

if (dx == 0.0)
return (self->line.by > self->line.ay) ? PyFloat_FromDouble(-90.0)
: PyFloat_FromDouble(90.0);

double dy = self->line.by - self->line.ay;
double gradient = dy / dx;

return PyFloat_FromDouble(-RAD_TO_DEG(atan(gradient)));
}

static PyObject *
pg_line_getslope(pgLineObject *self, void *closure)
{
double dem = self->line.bx - self->line.ax;
if (dem == 0) {
return PyFloat_FromDouble(0);
}

double slope = (self->line.by - self->line.ay) / dem;

return PyFloat_FromDouble(slope);
}

static PyGetSetDef pg_line_getsets[] = {
{"ax", (getter)pg_line_getax, (setter)pg_line_setax, DOC_LINE_AX, NULL},
{"ay", (getter)pg_line_getay, (setter)pg_line_setay, DOC_LINE_AY, NULL},
Expand All @@ -355,6 +464,14 @@ static PyGetSetDef pg_line_getsets[] = {
{"a", (getter)pg_line_geta, (setter)pg_line_seta, DOC_LINE_A, NULL},
{"b", (getter)pg_line_getb, (setter)pg_line_setb, DOC_LINE_B, NULL},
{"length", (getter)pg_line_getlength, NULL, DOC_LINE_LENGTH, NULL},
{"center", (getter)pg_line_get_center, (setter)pg_line_set_center,
DOC_LINE_CENTER, NULL},
{"centerx", (getter)pg_line_get_centerx, (setter)pg_line_set_centerx,
DOC_LINE_CENTERX, NULL},
{"centery", (getter)pg_line_get_centery, (setter)pg_line_set_centery,
DOC_LINE_CENTERY, NULL},
{"angle", (getter)pg_line_getangle, NULL, DOC_LINE_ANGLE, NULL},
{"slope", (getter)pg_line_getslope, NULL, DOC_LINE_SLOPE, NULL},
{NULL, 0, NULL, NULL, NULL}};

static PyTypeObject pgLine_Type = {
Expand Down
122 changes: 122 additions & 0 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2017,6 +2017,128 @@ def test_attrib_length(self):
expected_length = 5.414794548272353
self.assertAlmostEqual(line.length, expected_length)

def test_attrib_center(self):
"""a full test for the center attribute"""
expected_x1 = 10.0
expected_y1 = 2.0
expected_x2 = 5.0
expected_y2 = 6.0
expected_a = expected_x1, expected_y1
expected_b = expected_x2, expected_y2
expected_center = (
(expected_x1 + expected_x2) / 2,
(expected_y1 + expected_y2) / 2,
)
line = Line(expected_a, expected_b)

self.assertAlmostEqual(line.center, expected_center)

line.center = expected_center[0] - 1, expected_center[1] + 1.321

self.assertAlmostEqual(
line.center, (expected_center[0] - 1, expected_center[1] + 1.321)
)

line = Line(0, 0, 1, 0)

for value in (None, [], "1", (1,), [1, 2, 3], 1, 1.2):
with self.assertRaises(TypeError):
line.center = value

with self.assertRaises(AttributeError):
del line.center

def test_attrib_centerx(self):
"""a full test for the centerx attribute"""
expected_x1 = 10.0
expected_y1 = 2.0
expected_x2 = 5.0
expected_y2 = 6.0
expected_a = expected_x1, expected_y1
expected_b = expected_x2, expected_y2
expected_center = (
(expected_x1 + expected_x2) / 2,
(expected_y1 + expected_y2) / 2,
)
line = Line(expected_a, expected_b)

self.assertAlmostEqual(line.centerx, expected_center[0])

line.centerx = expected_center[0] - 1

self.assertAlmostEqual(line.centerx, expected_center[0] - 1)

line = Line(0, 0, 1, 0)

for value in (None, [], "1", (1,), [1, 2, 3]):
with self.assertRaises(TypeError):
line.centerx = value

with self.assertRaises(AttributeError):
del line.centerx

def test_attrib_centery(self):
"""a full test for the centery attribute"""
expected_x1 = 10.0
expected_y1 = 2.0
expected_x2 = 5.0
expected_y2 = 6.0
expected_a = expected_x1, expected_y1
expected_b = expected_x2, expected_y2
expected_center = (
(expected_x1 + expected_x2) / 2,
(expected_y1 + expected_y2) / 2,
)
line = Line(expected_a, expected_b)

self.assertAlmostEqual(line.centery, expected_center[1])

line.centery = expected_center[1] - 1.321

self.assertAlmostEqual(line.centery, expected_center[1] - 1.321)

line = Line(0, 0, 1, 0)

for value in (None, [], "1", (1,), [1, 2, 3]):
with self.assertRaises(TypeError):
line.centery = value

with self.assertRaises(AttributeError):
del line.centery

def test_attrib_angle(self):
"""a full test for the angle attribute"""
expected_angle = -83.93394864782331
line = Line(300.0, 400.0, 400.0, 1341.0)
self.assertAlmostEqual(line.angle, expected_angle)

expected_angle = 16.17215901578255
line = Line(300.0, 400.0, 400.0, 371.0)
self.assertAlmostEqual(line.angle, expected_angle)

expected_angle = -35.53767779197438
line = Line(45.0, 32.0, 94.0, 67.0)
self.assertAlmostEqual(line.angle, expected_angle)

expected_angle = -53.88065915052025
line = Line(544.0, 235.0, 382.0, 13.0)
self.assertAlmostEqual(line.angle, expected_angle)

def test_attrib_slope(self):
"""a full test for the slope attribute"""
lines = [
[Line(2, 2, 4, 4), 1, False],
[Line(4.6, 2.3, 1.6, 7.3), -5 / 3, True],
[Line(2, 0, 2, 1), 0, False],
[Line(1.2, 3.2, 4.5, 3.2), 0, False],
]

for l in lines:
if l[2]:
self.assertAlmostEqual(l[0].slope, l[1])
else:
self.assertAlmostEqual(l[0].slope, l[1])

def test_meth_copy(self):
line = Line(1, 2, 3, 4)
# check 1 arg passed
Expand Down
Loading