from typing import Tuple, Union
import pygame
import pygame.gfxdraw
import miniworldmaker.tokens.token as token
import miniworldmaker.tokens.token_plugins.shapes.shape_costume as shape_costume
from miniworldmaker.positions import position as board_position
from miniworldmaker.positions import vector as board_vector
from miniworldmaker.exceptions.miniworldmaker_exception import (
EllipseWrongArgumentsError,
LineFirstArgumentError,
LineSecondArgumentError,
RectFirstArgumentError,
)
[docs]
class Shape(token.Token):
"""Shape is the parent class for various geometric objects that can be created.
Each geometric object has the following properties:
* border: The border thickness of the object.
* is_filled: True/False if the object should be filled.
* fill_color: The fill color of the object
* border_color: The border color of the object.
.. image:: ../_images/shapes.png
:width: 60%
:alt: Shapes
"""
def __init__(self, position: tuple = None):
if position == None:
position = (0, 0)
super().__init__(position)
[docs]
def new_costume(self):
return shape_costume.ShapeCostume(self)
[docs]
class Circle(Shape):
"""
A circular shape, definied by position and radius
.. image:: ../_images/circle.png
:width: 120px
:alt: Circle
Args:
position: The position as 2-tuple. The circle is created with its center at the position
radius: The radius of the circle
Examples:
Create a circle at center position (200,100) with radius 20:
.. code-block:: python
Circle((200, 100), 20)
Create a circle at topleft position
.. code-block:: python
miniworldmaker.Circle.from_topleft((100,100),50)
"""
def __init__(self, position=(0, 0), radius: float = 10):
self._radius = radius
super().__init__(position)
self.position_manager.set_size((self._radius * 2, self._radius * 2), scale=False)
self.center = position
[docs]
@classmethod
def from_topleft(cls, position: tuple, radius: int):
"""Creates a circle with topleft at position"""
circle = cls(position, radius)
circle.topleft = circle.center[0], circle.center[1]
return circle
[docs]
@classmethod
def from_center(cls, position: tuple, radius: float):
"""Creates a circle with center at position"""
circle = cls(position, radius)
return circle
@property
def radius(self):
"""The radius of the circle.
If you change the circle-size (e.g. with self.size = (x, y), the radius value will be changed too.
"""
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
_center = self.center
self.position_manager.set_size((self._radius * 2, self._radius * 2), scale=False)
self.center = _center
self.costume.set_dirty("scale", self.costume.RELOAD_ACTUAL_IMAGE)
[docs]
def set_physics_default_values(self):
self.physics.shape_type = "circle"
self.physics.can_move = True
self.physics.stable = False
[docs]
def new_costume(self):
return shape_costume.CircleCostume(self)
[docs]
class Point(Circle):
"""A point is a Circle with Radius 1"""
[docs]
def __init__(self, position: tuple):
"""Init a Point at specified position"""
super().__init__(position, 1)
[docs]
class Ellipse(Shape):
"""An elliptic shape.
.. image:: ../_images/ellipse.png
:width: 120px
:alt: Ellipse
Args:
position: The position as 2-tuple. The ellipse is created at topleft position
width: The width of the ellipse
height: The height of the ellipse
Examples:
Create an ellipse at topleft position (200,100) with width 20 and height 30
.. code-block:: python
Ellipse((200, 100), 20, 30)
Create an ellipse at center-position (200,100) width width 10 and height 10
.. code-block:: python
miniworldmaker.Ellipse.from_center((100,100),10, 10)
(Alternative) Create an ellipse at center-position (200,100) with width 10 and height 10
.. code-block:: python
e = miniworldmaker.Ellipse((100,100),10, 10)
e.center = e.position
"""
def __init__(
self,
position=(0, 0),
width: float = 10,
height: float = 10,
):
self.check_arguments(position, width, height)
super().__init__(position)
self.costume = shape_costume.EllipseCostume(self)
self._border = 1
self.size = (width, height)
[docs]
def check_arguments(self, position, width, height):
if type(position) not in [tuple, board_position.Position, None]:
raise EllipseWrongArgumentsError()
[docs]
@classmethod
def from_topleft(cls, position: tuple, width: float, height: float):
"""Creates an ellipse with topleft at position"""
ellipse = cls(position, width, height)
return ellipse
[docs]
@classmethod
def from_center(cls, position: tuple, width: float, height: float):
"""Creates an ellipse with center at position"""
ellipse = cls(position, width, height)
ellipse.center = ellipse.position
return ellipse
class Arc(Ellipse):
"""
An elliptic Arc.
Args:
position: The position as 2-tuple. The ellipse is created at topleft position
width: The width of the ellipse
height: The height of the ellipse
start_angle: The start_angle
end_angle: end_angle
"""
def __init__(
self, position=(0, 0), width: float = 10, height: float = 10, start_angle: float = 0, end_angle: float = 0
):
self._start_angle = start_angle
self._end_angle = end_angle
if start_angle == end_angle:
self._end_angle = start_angle + 360
super().__init__(position, width, height)
self.costume = shape_costume.ArcCostume(self)
@property
def start_angle(self):
return self._start_angle
@start_angle.setter
def start_angle(self, value):
self._start_angle = value
self.costume.set_dirty("draw_shapes", self.costume.RELOAD_ACTUAL_IMAGE)
@property
def end_angle(self):
return self._end_angle
@end_angle.setter
def end_angle(self, value):
self._end_angle = value
self.costume.set_dirty("draw_shapes", self.costume.RELOAD_ACTUAL_IMAGE)
@classmethod
def from_topleft(cls, position: tuple, width: float, height: float, start_angle, end_angle):
"""Creates a rectangle with topleft at position"""
rectangle = cls(position, width, height, start_angle, end_angle).center
return rectangle
@classmethod
def from_center(cls, position: tuple, width: float, height: float, start_angle, end_angle):
"""Creates a rectangle with center at position"""
rectangle = cls(position, width, height, start_angle, end_angle)
rectangle.center = rectangle.position
return rectangle
[docs]
class Line(Shape):
"""A Line-Shape defined by start_position and end_position.
.. image:: ../_images/ellipse.png
:width: 120px
:alt: Line
Args:
start_position: The start_position as 2-tuple.
end_position: The end_position as 2-tuple.
Examples:
Create a line from (200, 100) to (400, 100)
.. code-block:: python
Line((200, 100), (400,100))
Create a line from (200, 100) to (400, 100)
.. code-block:: python
l = Line((200, 100), (400,100))
l.border = 2
"""
def __init__(self, start_position: Union[tuple, "board_position.Position"],
end_position: Union[tuple, "board_position.Position"]):
if not start_position or not end_position:
start_position = (0, 0)
end_position = (0, 0)
if type(start_position) not in [tuple, board_position.Position, None]:
raise LineFirstArgumentError(start_position)
if type(end_position) not in [tuple, board_position.Position, None]:
raise LineSecondArgumentError(end_position)
self._length = 0
self._start_position = board_position.Position.create(start_position)
self._end_position = board_position.Position.create(end_position)
super().__init__(start_position)
self.costume = shape_costume.LineCostume(self)
self._update_size()
@property
def start_position(self):
return self._start_position
start = start_position
@start_position.setter
def start_position(self, value):
self._start_position = board_position.Position.create(value)
self._update_size()
@property
def end_position(self):
return self._end_position
end = end_position
@end_position.setter
def end_position(self, value):
self._end_position = board_position.Position.create(value)
self._update_size()
@property
def direction(self):
return self.position_manager.get_direction()
@direction.setter
def direction(self, value):
self.position_manager.set_direction(value)
direction_vector = board_vector.Vector.from_direction(self.direction)
direction_vector = direction_vector.normalize() * self._length * 0.5
self._start_position = board_position.Position.create(self.center + direction_vector)
self._end_position = board_position.Position.create(self.center - direction_vector)
[docs]
def set_physics_default_values(self):
self.physics.shape_type = "line"
self.physics.simulation = "manual"
[docs]
def get_bounding_box(self):
width = abs(self.start_position[0] - self.end_position[0]) + self.thickness
height = abs(self.start_position[1] - self.end_position[1]) + self.thickness
box = pygame.Rect(
min(self.start_position[0], self.end_position[0]) - int(0.5 * self.thickness),
min(self.start_position[1], self.end_position[1]) - int(0.5 * self.thickness),
width,
height,
)
return box
def _update_size(self):
self._length = self.start_position.distance_to(self._end_position)
self.position_manager.set_size((self.thickness, self._length + 2 * self.thickness), scale=False)
self.position_manager.set_direction(self.start_position.direction_to(self._end_position).value)
self.center = self.start_position + board_vector.Vector.from_positions(self.start_position,
self.end_position) * 0.5
self.costume.set_dirty("all", 1)
@property
def length(self):
return self._length
@property
def thickness(self):
"""-> see border"""
return self.costume.border
@thickness.setter
def thickness(self, value):
self.costume.border = value
self._update_size()
@property
def border(self):
"""-> see border"""
return self.costume.border
@border.setter
def border(self, value):
self.costume.border = value
self._update_size()
line_width = thickness
[docs]
class Rectangle(Shape):
"""
A rectangular shape defined by position, width and height
.. image:: ../_images/ellipse.png
:width: 120px
:alt: Line
Args:
topleft: Topleft Position of Rect
height: The height of the rect
width: The width of the rect
Examples:
Create a rect with the topleft position (200, 100), the width 20 and the height 10
.. code-block:: python
Rectangle((200, 100), 20, 10)
"""
def __init__(
self,
topleft=(0, 0),
width: float = 10,
height: float = 10,
):
self.check_arguments(topleft, width, height)
super().__init__(topleft)
self.costume = shape_costume.RectangleCostume(self)
self.size = (width, height)
[docs]
def check_arguments(self, topleft, width, height):
if type(topleft) != tuple and type(topleft) != board_position.Position:
raise RectFirstArgumentError(topleft)
if type(width) not in [int, float]:
raise TypeError("width of Rectangle should be int or float " + str(type(width)))
if type(height) not in [int, float]:
raise TypeError("height of Rectangle should be int or float but is " + str(type(height)))
[docs]
def set_physics_default_values(self):
self.physics.shape_type = "rect"
self.physics.stable = False
self.physics.correct_angle = 90
[docs]
@classmethod
def from_topleft(cls, position: tuple, width: float, height: float):
"""Creates a rectangle with topleft at position"""
rectangle = cls(position, width, height).center
return rectangle
[docs]
@classmethod
def from_center(cls, position: tuple, width: float, height: float):
"""Creates a rectangle with center at position"""
rectangle = cls(position, width, height)
rectangle.center = rectangle.position
return rectangle
[docs]
class Polygon(Shape):
"""
A Polygon-Shape.
Args:
point-list: A list of points
Examples:
Example Creation of a polygon
>>> Polygon([(200, 100), (400,100), (0, 0)])
Creates a red polygon with the vertices (200, 100) , (400, 100) and (0, 0)
Example Creation of a filled polygon
>>> Polygon([(200, 100), (400,100), (0, 0)])
Creates a red polygon with the vertices (200, 100) , (400, 100) and (0, 0)
"""
def __init__(self, pointlist):
super().__init__((0, 0))
self._pointlist = pointlist
self.costume = shape_costume.PolygonCostume(self, pointlist)
@property
def pointlist(self):
return self._pointlist
@pointlist.setter
def pointlist(self, value: int):
self._pointlist = value
class Triangle(Polygon):
def __init__(self, p1: Tuple, p2: Tuple, p3: Tuple):
pointlist = [p1, p2, p3]
super().__init__(pointlist)