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

Line flip() / flip_ip() #222

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ other objects.

as_points: returns the line as a list of points.

flip: Flips the line along the given axes.

flip_ip: Flips the line along the given axes in place.

Additionally to these, the line shape can also be used as a collider for the ``geometry.raycast`` function.

Polygon
Expand Down
36 changes: 35 additions & 1 deletion docs/line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,38 @@ Line Methods
least 0.


.. ## Line.as_points ##
.. ## Line.as_points ##

.. method:: flip

| :sl:`flips the line`
| :sg:`flip(x, y, flip_around) -> Line`

Returns a new Line that is flipped horizontally and/or vertically. The original
Line is not modified. The flipping is done relative to the given point.
By default, the flipping is done relative to the center of the `Line`.

.. note::
If `x` is True, the Line will be flipped horizontally.
If `y` is True, the Line will be flipped vertically.
If `x` and `y` are both True, the Line will be flipped
horizontally and vertically.

.. ## Line.flip ##

.. method:: flip_ip

| :sl:`flips the line, in place`
| :sg:`flip_ip(x, y, flip_around) -> None`

Flips the Line horizontally and/or vertically. The original Line is modified.
The flipping is done relative to the given point. By default, the flipping is done
relative to the center of the `Line`. Always returns None.

.. note::
If `x` is True, the Line will be flipped horizontally.
If `y` is True, the Line will be flipped vertically.
If `x` and `y` are both True, the Line will be flipped
horizontally and vertically.

.. ## Line.flip_ip ##
6 changes: 6 additions & 0 deletions geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ class Line(Sequence[float]):
def is_perpendicular(self, line: LineValue) -> bool: ...
def as_points(self, n_points: int) -> List[Tuple[float, float]]: ...
def as_segments(self, n_segments: int) -> List[Line]: ...
def flip(
self, x: bool, y: bool = False, flip_around: Coordinate = Line.center
) -> Line: ...
def flip_ip(
self, x: bool, y: bool = False, flip_around: Coordinate = Line.center
) -> None: ...

class Circle:
x: float
Expand Down
88 changes: 86 additions & 2 deletions src_c/line.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ _pg_line_subtype_new4(PyTypeObject *type, double xa, double ya, double xb,
return (PyObject *)line;
}

static pgLineObject *
_pg_line_subtype_new(PyTypeObject *type, pgLineBase *line)
{
pgLineObject *lineobj =
(pgLineObject *)pgLine_Type.tp_new(type, NULL, NULL);
if (lineobj) {
lineobj->line = *line;
}
return lineobj;
}

static PyObject *
pg_line_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
Expand Down Expand Up @@ -476,7 +487,7 @@ pg_line_at(pgLineObject *self, PyObject *obj)
}

static PyObject *
pg_line_flip(pgLineObject *self, PyObject *_null)
pg_line_flip_ab(pgLineObject *self, PyObject *_null)
{
return _pg_line_subtype_new4(Py_TYPE(self), self->line.xb, self->line.yb,
self->line.xa, self->line.ya);
Expand Down Expand Up @@ -745,6 +756,77 @@ pg_line_as_circle(pgLineObject *self, PyObject *_null)
return (PyObject *)circle_obj;
}

static void
pg_line_flip_helper(pgLineBase *line, int dirx, int diry, double c_x,
double c_y)
{
double xa = line->xa;
double ya = line->ya;
double xb = line->xb;
double yb = line->yb;

if (dirx) {
line->xa = c_x - (xa - c_x);
line->xb = c_x - (xb - c_x);
}

if (diry) {
line->ya = c_y - (ya - c_y);
line->yb = c_y - (yb - c_y);
}
}

#define FLIP_PREP \
pgLineBase *line = &self->line; \
int dirx, diry = 0; \
double c_x, c_y; \
if (!nargs || nargs > 3) { \
return RAISE( \
PyExc_TypeError, \
"Invalid number of arguments, expected 1, 2 or 3 arguments"); \
} \
dirx = PyObject_IsTrue(args[0]); \
if (nargs > 1) { \
diry = PyObject_IsTrue(args[1]); \
} \
if (nargs == 3) { \
if (!pg_TwoDoublesFromObj(args[2], &c_x, &c_y)) { \
return RAISE(PyExc_TypeError, \
"Invalid flip point argument, must be a sequence " \
"of 2 numbers"); \
} \
} \
else { \
c_x = (self->line.xa + self->line.xb) / 2; \
c_y = (self->line.ya + self->line.yb) / 2; \
}

static PyObject *
pg_line_flip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
{
FLIP_PREP

pgLineObject *line_obj = _pg_line_subtype_new(Py_TYPE(self), line);
if (!line_obj) {
return NULL;
}

pg_line_flip_helper(&line_obj->line, dirx, diry, c_x, c_y);

return (PyObject *)line_obj;
}

static PyObject *
pg_line_flip_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs)
{
FLIP_PREP

pg_line_flip_helper(&self->line, dirx, diry, c_x, c_y);

Py_RETURN_NONE;
}
#undef FLIP_PREP

static struct PyMethodDef pg_line_methods[] = {
{"__copy__", (PyCFunction)pg_line_copy, METH_NOARGS, NULL},
{"copy", (PyCFunction)pg_line_copy, METH_NOARGS, NULL},
Expand All @@ -763,13 +845,15 @@ static struct PyMethodDef pg_line_methods[] = {
{"move", (PyCFunction)pg_line_move, METH_FASTCALL, NULL},
{"move_ip", (PyCFunction)pg_line_move_ip, METH_FASTCALL, NULL},
{"at", (PyCFunction)pg_line_at, METH_O, NULL},
{"flip_ab", (PyCFunction)pg_line_flip, METH_NOARGS, NULL},
{"flip_ab", (PyCFunction)pg_line_flip_ab, METH_NOARGS, NULL},
{"flip_ab_ip", (PyCFunction)pg_line_flip_ab_ip, METH_NOARGS, NULL},
{"as_points", (PyCFunction)pg_line_as_points, METH_O, NULL},
{"as_segments", (PyCFunction)pg_line_as_segments, METH_O, NULL},
{"scale", (PyCFunction)pg_line_scale, METH_FASTCALL, NULL},
{"scale_ip", (PyCFunction)pg_line_scale_ip, METH_FASTCALL, NULL},
{"as_circle", (PyCFunction)pg_line_as_circle, METH_NOARGS, NULL},
{"flip", (PyCFunction)pg_line_flip, METH_FASTCALL, NULL},
{"flip_ip", (PyCFunction)pg_line_flip_ip, METH_FASTCALL, NULL},
{NULL, NULL, 0, NULL}};

/* sequence functions */
Expand Down
123 changes: 122 additions & 1 deletion test/test_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ def get_points_between(line, n_pts):
return [(line.xa + i * dx, line.ya + i * dy) for i in range(n_pts + 2)]


def _flip_line(line, flip_x, flip_y=False, flip_center=None):
points = [*line.a, *line.b]
f_x, f_y = flip_center if flip_center is not None else line.center

if flip_x:
points[0] = f_x - (points[0] - f_x)
points[2] = f_x - (points[2] - f_x)
if flip_y:
points[1] = f_y - (points[1] - f_y)
points[3] = f_y - (points[3] - f_y)

return Line(*points)


class LineTypeTest(unittest.TestCase):
class ClassWithLineAttrib:
def __init__(self, line):
Expand Down Expand Up @@ -736,7 +750,7 @@ def test_meth_scale_ip(self):
with self.assertRaises(ValueError):
line.scale_ip(17, 10.0)

def test_meth_flip(self):
def test_meth_flip_ab(self):
line = Line(1.1, 2.2, 3.3, 4.4)

ret = line.flip_ab()
Expand Down Expand Up @@ -1381,6 +1395,113 @@ def test_meth_as_segments_argvalue(self):
with self.assertRaises(ValueError):
l.as_segments(value)

def test_meth_flip_flipip_argnum(self):
"""Tests if the function correctly handles incorrect number of parameters"""
l = Line(0, 0, 1, 1)

args = [
(),
(1, 2, 3, 4),
(1, 2, 3, 4, 5),
(1, 2, 3, 4, 5, 6),
]

for value in args:
with self.assertRaises(TypeError):
l.flip(*value)
with self.assertRaises(TypeError):
l.flip_ip(*value)

def assert_lines_equal(self, line1, line2, eps=1e-12):
self.assertAlmostEqual(line1.xa, line2.xa, delta=eps)
self.assertAlmostEqual(line1.ya, line2.ya, delta=eps)
self.assertAlmostEqual(line1.xb, line2.xb, delta=eps)
self.assertAlmostEqual(line1.yb, line2.yb, delta=eps)

def test_meth_flip(self):
"""Tests if the function correctly flips the line"""
l = Line(0, 0, 10, 10)

flip_points = [
(10, 234),
(-10, 234),
(10, -234),
(-10, -234),
]

# x axis
self.assert_lines_equal(l.flip(True), _flip_line(l, True))
self.assert_lines_equal(l.flip(True, False), _flip_line(l, True, False))
for point in flip_points:
self.assert_lines_equal(
l.flip(True, False, point), _flip_line(l, True, False, point)
)

# y axis
self.assert_lines_equal(l.flip(False, True), _flip_line(l, False, True))
for point in flip_points:
self.assert_lines_equal(
l.flip(False, True, point), _flip_line(l, False, True, point)
)

# both axes
self.assert_lines_equal(l.flip(True, True), _flip_line(l, True, True))
for point in flip_points:
self.assert_lines_equal(
l.flip(True, True, point), _flip_line(l, True, True, point)
)

def test_meth_flip_ip(self):
"""Tests if the function correctly flips the line"""
line = Line(0, 0, 10, 10)

flip_points = [
(10, 234),
(-10, 234),
(10, -234),
(-10, -234),
]

# x axis
l = line.copy()
flipped = _flip_line(l, True)
l.flip_ip(True)
self.assert_lines_equal(l, flipped)
l = line.copy()
flipped = _flip_line(l, True, False)
l.flip_ip(True, False)
self.assert_lines_equal(l, flipped)

for point in flip_points:
l = line.copy()
flipped = _flip_line(l, True, False, point)
l.flip_ip(True, False, point)
self.assert_lines_equal(l, flipped)

# y axis
l = line.copy()
flipped = _flip_line(l, False, True)
l.flip_ip(False, True)
self.assert_lines_equal(l, flipped)

for point in flip_points:
l = line.copy()
flipped = _flip_line(l, False, True, point)
l.flip_ip(False, True, point)
self.assert_lines_equal(l, flipped)

# both axes
l = line.copy()
flipped = _flip_line(l, True, True)
l.flip_ip(True, True)
self.assert_lines_equal(l, flipped)

for point in flip_points:
l = line.copy()
flipped = _flip_line(l, True, True, point)
l.flip_ip(True, True, point)
self.assert_lines_equal(l, flipped)


if __name__ == "__main__":
unittest.main()