import paranoid as pns
import math
class Metric(pns.Type):
"""A Paranoid Scientist Type for Points and Vectors."""
def test(self, v):
assert isinstance(v, Point) or isinstance(v, Vector)
def generate(self):
yield Point(0, 0)
yield Point(.3, .1, "absolute")
yield Point(.1, .1, "figure")
yield Point(-.1, .1, "figure")
yield Point(-.1, -3)
yield Point(2, 2)
yield Width(.3)
yield Width(-.1)
yield Width(0, "figure")
yield Height(-1, "absolute")
yield Height(2, "unique")
[docs]
@pns.paranoidclass
class Point:
"""A point on the canvas in an arbitrary coordinate system.
A point represents a specific location on the canvas. It
represents an x and y coordinate in a specific coordinate system,
given by `x`, `y`, and `coordinate`, respectively. Coordinate
systems are specified as strings, and are interpreted according to
a Canvas object.
Points and vectors can be added or subtracted to produce another
point. Points cannot be multiplied or divided by scalars. Points
cannot be added to each other, but they can be subtracted to
produce the vector connecting the two points.
"""
def __new__(cls, x, y, coordinate="default"):
if isinstance(coordinate, tuple):
return Point(x, 0, coordinate[0]) >> Point(0, y, coordinate[1])
else:
obj = object.__new__(cls)
obj.x = x
obj.y = y
obj.coordinate = coordinate
return obj
def __repr__(self):
nondefault = f', "{self.coordinate}"' if self.coordinate != "default" else ""
return f'{self.__class__.__name__}({self.x}, {self.y}{nondefault})'
[docs]
@pns.accepts(pns.Self, Metric)
def __add__(self, other):
"""Add together a point and a vector.
`other` should be a Vector, otherwise this function will throw an error.
Returns a Point
"""
if isinstance(other, Vector):
if self.coordinate == other.coordinate and self.coordinate != "various":
return Point(self.x + other.x, self.y + other.y, self.coordinate)
else:
return BinopPoint(self, '+', other)
raise ValueError(f"Invalid addition between {self!r} and {other!r}.")
[docs]
@pns.accepts(pns.Self, Metric)
def __sub__(self, other):
"""Find the vector which connects two points.
`other` should be a Point, otherwise this function will throw an error.
Returns a Vector.
"""
if isinstance(other, Vector):
if self.coordinate == other.coordinate:
return Point(self.x - other.x, self.y - other.y, self.coordinate)
else:
return BinopPoint(self, '-', other)
elif isinstance(other, Point):
if self.coordinate == other.coordinate:
return Vector(self.x - other.x, self.y - other.y, self.coordinate)
else:
return BinopVector(self, '-', other)
raise ValueError(f"Invalid subtraction between {self!r} and {other!r}.")
[docs]
def __eq__(self, other):
"""Determine if two Point objects are equal.
`other` should be another Point object.
Returns True or False.
"""
return (self.x == other.x) and (self.y == other.y) and (self.coordinate == other.coordinate)
def __iter__(self):
yield self.x
yield self.y
[docs]
def __rshift__(self, other):
"""Take the x coordinate of this point and the y coordinate of another point.
`other` should be a Point, otherwise this function will throw an error.
Returns a Point.
"""
if not isinstance(other, Point):
raise ValueError(f"Invalid meet >> operation between {self!r} and {other!r}.")
if self.coordinate == other.coordinate:
return Point(self.x, other.y, self.coordinate)
else:
return BinopPoint(self, '>>', other)
[docs]
def __lshift__(self, other):
"""Take the y coordinate of this point and the x coordinate of another point.
`other` should be a Point, otherwise this function will throw an error.
Returns a Point.
"""
return other >> self
[docs]
def __or__(self, other):
"""Return the point in the center of the two given points
`other` should be a Point, otherwise this function will throw an error.
Returns a Point.
"""
if not isinstance(other, Point):
raise ValueError(f"Invalid mean | operation between {self!r} and {other!r}.")
if self.coordinate == other.coordinate:
return Point((self.x+other.x)/2, (self.y+other.y)/2, self.coordinate)
else:
return BinopPoint(self, '|', other)
@staticmethod
def _generate():
yield Point(0, 0)
yield Point(.3, .1, "absolute")
yield Point(.1, .1, "figure")
[docs]
@pns.paranoidclass
class Vector:
"""A vector in an arbitrary coordinate system.
A Vector represents a difference in location on the canvas. It
represents a width and a height in a specific coordinate system,
given by `x`, `y`, and `coordinate`, respectively. Coordinate
systems are specified as strings, and are interpreted according to
a Canvas object.
Vectors can be added or subtracted to other vectors or points.
They can also be multiplied and divided by scalars.
A Vector which only has an x component is automatically cast to a
"Width" object, and with only a y component is a "Height" object.
"""
def __new__(cls, x, y, coordinate="default"):
if isinstance(coordinate, tuple):
return Vector(x, 0, coordinate[0]) >> Vector(0, y, coordinate[1])
else:
obj = object.__new__(cls)
obj.x = x
obj.y = y
obj.coordinate = coordinate
return obj
def __repr__(self):
nondefault = f', "{self.coordinate}"' if self.coordinate != "default" else ""
return f'{self.__class__.__name__}({self.x}, {self.y}{nondefault})'
[docs]
@pns.accepts(pns.Self, Metric)
def __add__(self, other):
"""Add a vector to a Point or another Vector.
If `other` is a Point, return a Point. If `other` is a Vector, return a Vector.
"""
if isinstance(other, Point):
return other + self
elif isinstance(other, Vector):
if self.coordinate == other.coordinate:
return Vector(self.x + other.x, self.y + other.y, self.coordinate)
else:
return BinopVector(self, '+', other)
raise ValueError(f"Invalid addition between {repr(self)} and {repr(other)}.")
[docs]
@pns.accepts(pns.Self, pns.Self)
def __sub__(self, other):
"""Vector subtraction.
`other` should be a Vector.
Return a Vector.
Subtracting a vector is equivalent to adding the negative of the vector.
"""
if self.coordinate == other.coordinate:
return Vector(self.x - other.x, self.y - other.y, self.coordinate)
else:
return BinopVector(self, '-', other)
[docs]
@pns.accepts(pns.Self, pns.Number)
def __mul__(self, other):
"""Multiply a vector by a scalar.
`other` should be a scalar by which to multiply each component of the vector.
Return a Vector.
"""
return Vector(self.x * other, self.y * other, self.coordinate)
@pns.accepts(pns.Self, pns.Number)
def __rmul__(self, other):
self.__mul__.__doc__
return self * other
[docs]
@pns.accepts(pns.Self)
def __neg__(self):
"""Take the negative of a vector.
This is equivalent to rotating the vector by 180 degrees, or
to multiplying each component by -1.
Return a Vector.
"""
return -1* self
[docs]
@pns.accepts(pns.Self, pns.Number)
@pns.requires("other != 0")
def __truediv__(self, other):
"""Divide a vector by a scalar.
`other` should be a non-zero scalar by which to divide each component of the vector.
Return a Vector.
"""
return Vector(self.x / other, self.y / other, self.coordinate)
[docs]
@pns.accepts(pns.Self, pns.Self)
def __rshift__(self, other):
"""Take the x coordinate of this vector and the y coordinate of another vector.
`other` should be a Vector, otherwise this function will throw an error.
Returns a Vector.
"""
if not isinstance(other, Vector):
raise ValueError(f"Invalid meet >> operation between {repr(self)} and {repr(other)}.")
if self.coordinate == other.coordinate:
return Vector(self.x, other.y, self.coordinate)
else:
return BinopVector(self, '>>', other)
[docs]
@pns.accepts(pns.Self, pns.Self)
def __lshift__(self, other):
"""Take the y coordinate of this vector and the x coordinate of another vector.
`other` should be a Vector, otherwise this function will throw an error.
Returns a Vector.
"""
return other >> self
[docs]
@pns.accepts(pns.Self, pns.Number)
def __matmul__(self, other):
"""Rotate the vector by some amount, specified in degrees.
`other` should be a scalar, in units of degrees.
Returns a Vector.
"""
if self.coordinate == "absolute":
c = math.cos(math.radians(other))
s = math.sin(math.radians(other))
return Vector(self.x * c - self.y * s ,
self.x * s + self.y * c,
self.coordinate)
else:
return BinopVector(self, '@', other)
@pns.accepts(pns.Self, pns.Number)
def __rmatmul__(self, other):
self.__matmul__.__doc__
return self @ other
[docs]
@pns.accepts(pns.Self)
def width(self):
"""Returns a Width object representing the x component of the Vector."""
return Width(self.x, self.coordinate)
[docs]
@pns.accepts(pns.Self)
def height(self):
"""Returns a Height object representing the y component of the Vector."""
return Height(self.y, self.coordinate)
[docs]
@pns.accepts(pns.Self)
def flipx(self):
"""Returns a Vector reflected across the y-axis."""
return Vector(-self.x, self.y, self.coordinate)
[docs]
@pns.accepts(pns.Self)
def flipy(self):
"""Returns a Vector reflected across the x-axis."""
return Vector(self.x, -self.y, self.coordinate)
[docs]
@pns.accepts(pns.Self, pns.Self)
def __eq__(self, other):
"""Determine if two Vector objects are equal.
`other` should be another Vector object.
Returns True or False.
"""
return (self.x == other.x) and (self.y == other.y) and (self.coordinate == other.coordinate)
def __iter__(self):
yield self.x
yield self.y
@classmethod
def _generate(cls):
yield Height(.3)
yield Width(.5)
[docs]
def Width(x, coordinate="default"):
"""A vector with a 0 in the y coordinate.
Returns a Vector with `x` in the x coordinate and 0 in the y
coordinate, within the coordinate system `coordinate`.
This is included for backward compatibility.
"""
return Vector(x, 0, coordinate)
[docs]
def Height(y, coordinate="default"):
"""A vector with a 0 in the x coordinate.
Returns a Vector with 0 in the x coordinate and `y` in the y
coordinate, within the coordinate system `coordinate`.
This is included for backward compatibility.
"""
return Vector(0, y, coordinate)
[docs]
@pns.paranoidclass
class BinopPoint(MetaBinop,Point):
"""A Point which is a composite of points and vectors in different bases.
Each Vector and Point's coordinate system depends on the Canvas
object, and thus, we cannot immediately compute sums or
differences of Vector or Point objects. Instead, this object
represents a node in a tree of operations on Vector and Point
objects which results in a Point object.
"""
@pns.accepts(pns.Self, Vector)
def __add__(self, rhs):
if isinstance(rhs, Vector):
return BinopPoint(self, '+', rhs)
@pns.accepts(pns.Self, Metric)
def __sub__(self, rhs):
if isinstance(rhs, Vector):
return BinopPoint(self, '-', rhs)
elif isinstance(rhs, Point):
return BinopVector(self, '-', rhs)
def __lshift__(self, rhs):
return rhs >> self
def __rshift__(self, rhs):
if isinstance(rhs, Point):
return BinopPoint(self, '>>', rhs)
else:
raise ValueError(f"Invalid meet >> between {repr(self)} and {repr(other)}.")
def __or__(self, rhs):
if isinstance(rhs, Point):
return BinopPoint(self, '|', rhs)
else:
raise ValueError(f"Invalid mean | between {repr(self)} and {repr(other)}.")
[docs]
@pns.paranoidclass
class BinopVector(MetaBinop,Vector):
"""A Vector which is a composite of points and vectors in different bases.
Each Vector and Point's coordinate system depends on the Canvas
object, and thus, we cannot immediately compute sums or
differences of Vector or Point objects. Instead, this object
represents a node in a tree of operations on Vector and Point
objects which results in a Vector object.
"""
@pns.accepts(pns.Self, Metric)
def __add__(self, rhs):
if isinstance(rhs, Vector):
return BinopVector(self, '+', rhs)
elif isinstance(rhs, Point):
return rhs + self
@pns.accepts(pns.Self, Vector)
def __sub__(self, rhs):
return BinopVector(self, '-', rhs)
@pns.accepts(pns.Self, pns.Number)
def __mul__(self, rhs):
return BinopVector(self, '*', rhs)
@pns.accepts(pns.Self, pns.Number)
def __truediv__(self, rhs):
return BinopVector(self, '/', rhs)
def __rshift__(self, rhs):
if isinstance(rhs, Vector):
return BinopVector(self, '>>', rhs)
else:
raise ValueError(f"Invalid meet >> between {repr(self)} and {repr(other)}.")
def __lshift__(self, rhs):
return rhs >> self
@pns.accepts(pns.Self, pns.Number)
def __matmul__(self, rhs):
return BinopVector(self, '@', rhs)
def __rmatmul__(self, rhs):
rhs @ self
[docs]
def width(self):
"""Returns a BinopVector object representing the x component of the Vector."""
# Handle cases where we use a scalar instead of a vector
return ((Point(0, 0, "absolute") + self) >> Point(0, 0, "absolute")) - Point(0, 0, "absolute")
[docs]
def height(self):
"""Returns a BionpVector object representing the y component of the Vector."""
return ((Point(0, 0, "absolute") + self) << Point(0, 0, "absolute")) - Point(0, 0, "absolute")
def flipx(self):
return (-self) >> self
def flipy(self):
return self >> (-self)
@classmethod
def _generate(cls):
yield cls(Point(0, 1), '+', Point(3, 2, "absolute"))
yield cls(Point(-.2, .2), '-', cls(Point(0, -1, "absolute"), '+', Width(2, "otherunit")))