Initial commit

This commit is contained in:
Chris Proctor
2025-07-28 13:29:18 -04:00
commit f7fd3e8a7e
18 changed files with 1313 additions and 0 deletions

19
geometry/base.py Normal file
View File

@@ -0,0 +1,19 @@
from caseconverter import snakecase
class GeometryObject:
"""A base class for geometry objects.
This base class provides some consistent behavior for all geometry objects.
Note: This class uses some advanced techniques which you aren't expected to
understand. If you're interested, by all means keep reading, and ask a teacher
if you have questions.
"""
def __new__(cls, value):
"""Overrides default constructor to support construction from other objects.
When value is an instance of CustomClass, checks cls for a `from_custom_class`
method which will be called to produce a new instance.
"""
method_name = value.__class__.__name__

34
geometry/drawing.py Normal file
View File

@@ -0,0 +1,34 @@
from turtle import *
from superturtle.movement import fly
def centered_circle(radius):
"""Draws a circle, centered on the turtle's current position.
The built-in circle function annoyingly draws a circle whose center
is one radius left of the turtle.
"""
right(90)
penup()
forward(radius)
pendown()
left(90)
circle(radius)
left(90)
penup()
forward(radius)
pendown()
right(90)
def write_label(point, label, align=None, font=None):
fly(point.x, point.y)
write(label, align=align, font=font)
def line_end_arrow():
"Draws an arrow"
left(20)
back(20)
forward(20)
right(40)
back(20)
forward(20)
left(20)

73
geometry/point.py Normal file
View File

@@ -0,0 +1,73 @@
from geometry.validation import is_coordinates, expect_type
from geometry.drawing import centered_circle, write_label
from superturtle.movement import fly
from turtle import begin_fill, end_fill, fillcolor
class Point:
"""A Point is a location in 2D space, identified by its
x and y coordinates. A Point can be initialized in multiple ways:
- With no value: The Point will be the origin (0, 0).
- With an ordered pair of numbers: The Point will have those coordinates.
- With a Vector: The Point will have the Vector's x and y coordinates.
"""
def __init__(self, value=None):
from geometry.vector import Vector
if value is None:
self.x, self.y = (0, 0)
elif is_coordinates(value):
self.x, self.y = value
elif isinstance(value, Vector):
self.x, self.y = value.x, value.y
else:
raise TypeError(f"Can't create a Point from {value}.")
def draw(self, label=None):
fly(self.x, self.y)
fillcolor("black")
begin_fill()
centered_circle(2)
end_fill()
if label:
write_label(self, label)
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point(({self.x}, {self.y}))"
def __eq__(self, other):
"""Implements equality checking with a == b.
Two Points are equal if their coordinates are the same.
A Point can only be equal to another Point.
"""
try:
expect_type(other, Point)
return (self.x, self.y) == (other.x, other.y)
except TypeError:
return False
def __add__(self, other):
"""Implements addition with a + b. Other can be:
- a Vector: interpreted as moving the point in the direction of the Vector,
returning a new Point.
"""
from geometry.vector import Vector
expect_type(other, Vector)
return Point((self.x + other.x, self.y + other.y))
def __sub__(self, other):
"""Implements subtraction with a - b. Other can be:
- a Point: interpreted as "what's needed to get from here to there,
returning a Vector.
- a Vector:interpreted as moving the point in the (negative) direction
of the vector and returns a Point.
"""
from geometry.vector import Vector
expect_type(other, (Point, Vector))
if isinstance(other, Point):
return Vector((self.x - other.x, self.y - other.y))
elif isinstance(other, Vector):
return Point((self.x - other.x, self.y - other.y))

31
geometry/validation.py Normal file
View File

@@ -0,0 +1,31 @@
#
#
#
def is_numeric(value):
"Checks whether value is an integer or a float (decimal number)."
return isinstance(value, int) or isinstance(value, float)
def is_coordinates(value):
"Checks whether value is a tuple of two numeric values."
try:
x, y = value
return is_numeric(x) and is_numeric(y)
except (TypeError, ValueError):
return False
def has_method(obj, method_name):
"Checks whether the object has a method called `method_name`."
attr = getattr(obj, method_name, None)
return callable(attr)
def expect_type(obj, expected_type):
"""Checks whether obj is of the expected type.
If multiple types are provided, checks whether obj is one of the types.
"""
try:
if not any(isinstance(obj, t) for t in expected_type):
raise TypeError(f"Expected {obj} to be one of the following types: {expected_type}")
except TypeError:
if not isinstance(obj, expected_type):
raise TypeError(f"Expected {obj} to be a {expected_type}")

141
geometry/vector.py Normal file
View File

@@ -0,0 +1,141 @@
from geometry.point import Point
from geometry.validation import is_coordinates, expect_type
from geometry.drawing import line_end_arrow
from math import sqrt, atan
from superturtle.movement import fly
from turtle import setheading, forward
class Vector:
"""A Vector represents movement in 2D space. Like a Point, a
Vector is defined by x and y coordinates. However, Points and Vectors
are different. Here are a few things you can do with Vectors which it doesn't
make sense to do with Points:
- Multiply them by a number (multiplying a vector by 2 means twice the movement)
- Negate them (-v means moving in the opposite direction of v)
- Find their length
A Vector can be initialized in multiple ways:
- With no value: Represents no motion at all: (0, 0).
- With an ordered pair of numbers: The Vector will have those coordinates.
- With a Point: The Vector will have the Point's x and y coordinates.
"""
def __init__(self, value=None):
if value is None:
self.x, self.y = (0, 0)
elif is_coordinates(value):
self.x, self.y = value
elif isinstance(value, Point):
self.x, self.y = value.x, value.y
else:
raise TypeError(f"Can't create a Vector from {value}.")
def draw(self, origin=None):
"Draws the vector."
origin = origin or Point()
expect_type(origin, Point)
fly(origin.x, origin.y)
θ = atan(self.y / self.x)
setheading(θ)
forward(self.mag())
line_end_arrow()
def __str__(self):
return f"{self.x}, {self.y}"
def __repr__(self):
return f"Vector(({self.x}, {self.y}))"
def __eq__(self, other):
"""Implements equality checking with a == b.
Two Vectors are equal if their coordinates are the same.
A Vector can only be equal to another Vector.
"""
try:
expect_type(other, Vector)
return (self.x, self.y) == (other.x, other.y)
except TypeError:
return False
def __neg__(self):
"""Implements negation with -a.
"""
return Vector((-self.x, -self.y))
def __add__(self, other):
"""Implements addition with a + b. Other can be:
- a Vector: interpreted as combining the movement of self and other,
returning a new Vector.
- a Point: interpreted as moving the point in the direction of the Vector,
returning a new Point.
"""
expect_type(other, (Point, Vector))
if isinstance(other, Vector):
return Vector((self.x + other.x, self.y + other.y))
elif isinstance(other, Point):
return Point((self.x + other.x, self.y + other.y))
def __sub__(self, other):
"""Implements subtraction with a - b. Other must be a Vector,
interpreted as subtraction the movement of other from self and
returning a new Vector.
"""
expect_type(other, Vector)
return Vector((self.x - other.x, self.y - other.y))
def __mul__(self, other):
"""Implements multiplication with a * 2, interpreted as scaling the
vector by other. Other must be numeric.
"""
expect_type(other, (int, float))
return Vector((self.x * other, self.y * other))
def __rmul__(self, other):
"""Implements multiplication with 2 * a.
"""
return self * other
def __truediv__(self, other):
"""Implements division with a / b, interpreted as dividing the
vector by other. Other must be numeric.
"""
expect_type(other, (int, float))
if other == 0:
raise ZeroDivisionError("Can't divide a Vector by zero.")
return Vector((self.x / other, self.y / other))
def __bool__(self):
"""Implements truthiness--whether an instance of Vector is treated as
True or False. Any Vector other than the zero vector is True.
"""
return self.mag() != 0
def mag(self):
"""Returns the magnitude (length) of the vector, using the Pythagorean formula.
"""
return sqrt(self.x * self.x + self.y * self.y)
def scale(self, mag):
"""Returns a Vector pointing in the same direction with the given magnitude.
"""
expect_type(mag, (int, float))
if self.mag() > 0:
return self * (mag / self.mag())
else:
raise ValueError("Can't scale the zero vector.")
def normalize(self):
"""Returns a Vector pointing in the same direction with a magnitude of 1.
"""
return self.scale(1)
def dot(self, other):
"""Computes the dot product of this vector with another.
"""
expect_type(other, Vector)
return self.x * other.x + self.y * other.y
def project(self, other):
"""Projects this vector onto another.
"""
return other.scale(self.dot(other.normalize()))