A computational geometry learning and experimentation tool.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

193 lines
5.0 KiB

from math import floor
class Point:
"""
A class representing a point. A point
has a point_size bounding box around
it.
"""
def __init__(self, x, y, point_size):
"""
Initializes a new point with a point_size bounding box.
@param point_size The size of the point in pixels.
@param x The x-coordinate.
@param y The y-coordinate.
"""
self.__point_size = point_size
self.__x = x
self.__y = y
half_point = floor(point_size / 2)
self.__top_left_corner = (self.__x - half_point,
self.__y + half_point)
self.__bottom_right_corner = (self.__x + half_point,
self.__y - half_point)
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
@property
def point_size(self):
return self.__point_size
def __eq__(self, other):
"""
Override for class equality.
@param other The other object.
"""
return (self.__x == other.x and
self.__y == other.y and
self.__point_size == other.point_size)
def __hash__(self):
"""
Overridden hashing function so it can be used as a dictionary key
for attributes.
"""
return hash((self.__x, self.__y, self.__point_size))
def __repr__(self):
return "POINT<X :{} Y: {} SIZE: {}>".format(self.__x,
self.__y,
self.__point_size)
def hit(self, x, y):
"""
Determines if the point was hit inside of it's bounding box.
The condition for hit is simple - consider the following
bounding box:
-------------
| |
| (x,y) |
| |
-------------
Where the clicked location is in the center. Then the top
left corner is defined as (x - half_point_size, y + half_point_size)
and the bottom corner is (x + half_point_size, y - half_point_size)
So long as x and y are greater than the top left and less than the
top right it is considered a hit.
This function is necessary for properly deleting and selecting points.
"""
return (x >= self.__top_left_corner[0] and
x <= self.__bottom_right_corner[0] and
y <= self.__top_left_corner[1] and
y >= self.__bottom_right_corner[1])
class Attribute:
def __init__(self, name, value):
"""
Initializes an attribute.
"""
self.__name = name
self.__value = value
class PointSet:
"""
Useful container for points backed by a set to insure point
uniqueness.
"""
def __init__(self, point_size):
"""
Initializes a point container with points of size point_size.
@param point_size The size of the points.
"""
self.__points = set()
self.__attributes = {}
self.__point_size = point_size
@property
def points(self):
"""
Getter for points. Returns a generator for
looping.
"""
for point in self.__points:
yield point
@property
def point_size(self):
return self.__point_size
def add_point(self, x, y, attrs=[]):
"""
Adds a point in screen coordinates and an optional attribute to
the list.
@param x The x-coordinate.
@param y The y-coordinate.
@param attr An optional attribute.
"""
if attrs != [] and not all(isinstance(x, Attribute) for x in attrs):
raise ValueError("Attributes in add_point must be an " +
"attribute array.")
point = Point(x, y, self.__point_size)
self.__points.add(point)
self.__attributes[point] = attrs
def remove_point(self, x, y):
"""
Removes a point and it's attributes from the point set based
on a bounding box calculation.
Removing a point is an exercise is determining which points
have been hit, and then pulling them out of the list.
If two points have a section overlapping, and the user clicks
the overlapped section, both points will be removed.
Currently O(n).
@param x The x-coordinate.
@param y The y-coordinate.
"""
# Find points that match
matched = set(p for p in self.__points if p.hit(x, y))
# In place set difference
self.__points = self.__points - matched
# Remove associated attributes
for point in matched:
self.__attributes.pop(point)
def attributes(self, point):
"""
Returns the attribute array for a given point.
@param point The point to get attributes for..
"""
return self.__attributes[point]