doodles-py/src/doodles/lines.py

118 lines
3.6 KiB
Python
Raw Normal View History

2024-04-26 19:48:30 +00:00
"""
Class to draw lines.
This is the most well-documented version of a concrete doodle,
the easiest to learn from.
"""
2024-04-22 07:12:59 +00:00
import math
import random
from typing import Callable, Self
2024-04-22 07:12:59 +00:00
from .doodles import Doodle
from .world import world
2024-04-22 07:12:59 +00:00
2024-04-22 07:35:23 +00:00
2024-04-22 07:12:59 +00:00
class Line(Doodle):
2024-04-26 19:48:30 +00:00
# Adds one attribute to Doodle: the distance/offset vector
# from the position. Together these form the end points of the
# line.
_offset_vec: tuple[float, float]
2024-04-22 07:12:59 +00:00
def __init__(self, parent=None):
"""
We keep the same interface as Doodle, to follow the Liskov substitution
principle.
We could add more *optional* arguments, but no more required ones
than the parent class.
"""
super().__init__(parent)
# a line is stored as a position (on the parent class)
# and an offset vector
self._offset_vec = (10, 0)
def __repr__(self):
2024-04-23 03:54:18 +00:00
return f"Line(pos={self.world_vec}, end={self.end_vec}, {self._color})"
2024-04-22 07:12:59 +00:00
def draw(self):
2024-04-22 07:12:59 +00:00
"""
Implementation of the abstract draw function for the line.
2024-04-26 19:48:30 +00:00
This class passes the responsibility of actually drawing to
the world.draw_engine, which is designed to be configurable.
2024-04-22 07:12:59 +00:00
2024-04-26 19:48:30 +00:00
An earlier version had Pygame drawing code in here, but that
violated the Single Responsibility Principle.
2024-04-22 07:12:59 +00:00
2024-04-26 19:48:30 +00:00
Instead, as a pass-through, the actual drawing logic is not coupled
to the mathematical representation of a line.
2024-04-22 07:12:59 +00:00
"""
world.draw_engine.line_draw(self)
2024-04-22 07:12:59 +00:00
2024-04-26 19:48:30 +00:00
## Setters / Modifiers / Getters ##############
def to(self, x: float, y: float) -> Self:
2024-04-22 07:12:59 +00:00
"""
A setter for the line's offset vector.
Example usage:
Line().pos(10, 10).to(50, 50)
Makes a line from (10, 10) to (50, 50).
"""
self._offset_vec = (x, y)
return self
def vec(self, degrees: float, magnitude: float):
2024-04-22 07:12:59 +00:00
"""
2024-04-26 19:48:30 +00:00
Alternate setter, to create offset vector from angle & length.
This is similar to the constructor/alternate constructor concept
where there's a base constructor that sets the propertries
directly (`to`), but there is also an alternate option
that handles commonly used case.
2024-04-22 07:12:59 +00:00
"""
return self.to(
magnitude * math.cos(math.radians(degrees)),
magnitude * math.sin(math.radians(degrees)),
)
2024-04-22 07:35:23 +00:00
def degrees(self, degrees: float):
"""
Alternate setter, like calling vec(new_degrees, old_magnitude).
"""
magnitude = math.sqrt(self._offset_vec[0]**2 + self._offset_vec[1]**2)
2024-04-22 07:12:59 +00:00
return self.to(
magnitude * math.cos(math.radians(degrees)),
magnitude * math.sin(math.radians(degrees)),
)
def random(self) -> Self:
2024-04-22 07:12:59 +00:00
"""
2024-04-26 19:48:30 +00:00
Overrides the parent's random, since a random line
also needs to have a offset vector.
2024-04-22 07:12:59 +00:00
2024-04-26 19:48:30 +00:00
This is an example of the **Open/Closed Principle**.
2024-04-22 07:12:59 +00:00
We aren't modifying the parent classes' random function
since doing so would be fragile and break if the
parent class added more options.
Instead we just call it, and extend it with additional
randomization.
"""
super().random()
magnitude = random.random() * 100
degrees = random.random() * 360
return self.vec(degrees, magnitude)
@property
def end_vec(self):
"""
2024-04-26 19:48:30 +00:00
Line goes from world_x, world_y to this position which
results from adding (world_x, world_y) + (offset_x, offset_y).
2024-04-22 07:12:59 +00:00
"""
return (
2024-04-23 03:54:18 +00:00
self.world_x + self._offset_vec[0],
self.world_y + self._offset_vec[1],
2024-04-22 07:12:59 +00:00
)