#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Geometry management module.
Implements:
class WorldAnchorage2D -- geometrical anchorage for 2D world.
class WorldAnchorage3D -- geometrical anchorage for 3D world.
class WorldArea2D -- playground area for 2D world.
class WorldArea3D -- playground area for 3D world.
class WorldBoundingBox2D -- bounding box for 2D world.
class WorldBoundingBox3D -- bounding box for 3D world.
class WorldBoxBase2D -- base class for boxes in a 2D world.
class WorldBoxBase3D -- base class for boxes in a 3D world.
class WorldPoint2D -- geometrical point for 2D world.
class WorldPoint3D -- geometrical point for 3D world.
class WorldVector2D -- geometrical vector for 2D world.
class WorldVector3D -- geometrical vector for 3D world.
"""
"""
The MIT License (MIT)
Copyright (c) 2015 Raphaël SEBAN
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import itertools
import operator
[docs]class WorldAnchorage2D:
"""Geometrical anchorage for 2D world."""
# constant defs
C = CENTER = "c"
E = EAST = "e"
N = NORTH = "n"
S = SOUTH = "s"
W = WEST = "w"
LEGAL_ANCHORS = (C, E, N, N+E, N+W, S, S+E, S+W, W)
DEFAULT_ANCHOR = CENTER
DEFAULT_ORIGIN = None
DEFAULT_UNIT_VECTOR = (1, 1)
def __init__ (self, **options):
"""Class constructor.
Parameter:
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: CENTER).
origin -- anchorage point of origin (default: None).
unit_vector -- local unit vector (default: (1, 1)).
Notice: options are saved into self.options member attribute.
"""
self.options = options
self.anchor = options.get("anchor")
self.origin = options.get("origin")
self.unit_vector = options.get("unit_vector")
# end def
@property
def anchor (self):
"""Anchorage query - read-write attribute.
This must be one of self.LEGAL_ANCHORS e.g. CENTER, EAST,
NORTH, etc.
"""
return self.__anchor
# end def
@anchor.setter
[docs] def anchor (self, value):
value = str(value or self.DEFAULT_ANCHOR).lower()
if value in self.LEGAL_ANCHORS:
self.__anchor = value
else:
raise ValueError(
"anchor should be one of {}."
.format(self.LEGAL_ANCHORS)
)
# end if
# end def
@property
def origin (self):
"""Anchorage point of origin - read-write attribute."""
return self.__origin
# end def
@origin.setter
[docs] def origin (self, value):
self.__origin = self.point_type(value or self.DEFAULT_ORIGIN)
# end def
@property
[docs] def point_type (self):
"""Preferred point type - read-only attribute.
Current implementation returns WorldPoint2D class type.
This can be redefined in subclasses to fit more specific needs.
"""
return WorldPoint2D
# end def
@property
def unit_vector (self):
"""Local unit vector - read-write attribute."""
return self.__unit_vector
# end def
@unit_vector.setter
[docs] def unit_vector (self, value):
self.__unit_vector = self.vector_type(
value or self.DEFAULT_UNIT_VECTOR
)
# end def
@property
[docs] def vector_type (self):
"""Preferred vector type - read-only attribute.
Current implementation returns WorldVector2D class type.
This can be redefined in subclasses to fit more specific needs.
"""
return WorldVector2D
# end def
# end class
[docs]class WorldAnchorage3D (WorldAnchorage2D):
"""Geometrical anchorage for 3D world."""
# constant defs
F = FRONT = "f"
M = MIDDLE = "m"
R = REAR = "r"
LEGAL_ANCHORS = tuple(
a + b
for a in (F, M, R)
for b in WorldAnchorage2D.LEGAL_ANCHORS
)
DEFAULT_ANCHOR = MIDDLE + WorldAnchorage2D.CENTER
DEFAULT_ORIGIN = None
DEFAULT_UNIT_VECTOR = (1, 1, 1)
@property
[docs] def point_type (self):
"""Preferred point type - read-only attribute.
Current implementation returns WorldPoint3D class type.
This can be redefined in subclasses to fit more specific needs.
"""
return WorldPoint3D
# end def
@property
[docs] def vector_type (self):
"""Preferred vector type - read-only attribute.
Current implementation returns WorldVector3D class type.
This can be redefined in subclasses to fit more specific needs.
"""
return WorldVector3D
# end def
# end class
[docs]class WorldBoxBase2D:
"""Base class for boxes in a 2D world."""
def __getitem__ (self, key):
"""Make object iterable."""
return self.to_tuple()[key]
# end def
def __init__ (self, width=0, height=0, **options):
"""Class constructor.
Parameter:
width -- box' width (default: 0).
height -- box' height (default: 0).
options -- keyword arguments.
Supported keyword arguments:
-- none at this stage --
Notice: options are saved into self.options member attribute.
"""
try:
(self.width, self.height) = width
except:
(self.width, self.height) = (width, height)
# end try
self.options = options
# end def
def __repr__ (self):
"""Object string representation."""
return str(self.to_tuple())
# end def
@property
[docs] def center_x (self):
"""Center of X dimension - read-only attribute.
Current implementation merely returns self.width//2.
"""
return self.width//2
# end def
@property
[docs] def center_xy (self):
"""Tuple center of (X, Y) dimensions - read-only attribute.
Current implementation merely returns a tuple of
(self.center_x, self.center_y).
"""
return (self.center_x, self.center_y)
# end def
@property
[docs] def center_y (self):
"""Center of Y dimension - read-only attribute.
Current implementation merely returns self.height//2.
"""
return self.height//2
# end def
@property
def height (self):
"""Box height - read-write attribute."""
return self.__height
# end def
@height.setter
[docs] def height (self, value):
self.__height = self.normalize_dimension(value)
# end def
[docs] def normalize_dimension (self, value):
"""Format value in order to fit with dimension constraints.
Parameter:
value -- should be a numeric value (int or float).
Will raise ValueError if 'value' is not a numeric value.
Returns 0 if bool(value) is False.
"""
if value:
if isinstance(value, (int, float)):
return abs(value)
else:
raise ValueError(
"Value must be of int or float type, not '{}'."
.format(value)
)
# end if
# end if
return 0
# end def
@property
[docs] def size (self):
"""Box size - read-only attribute.
Returns (width, height) tuple.
"""
return (self.width, self.height)
# end def
[docs] def to_dict (self):
"""Dictionary representation of object attributes.
Returns dict(width=self.width, height=self.height).
"""
return dict(width=self.width, height=self.height)
# end def
[docs] def to_tuple (self):
"""Tuple representation of object attributes.
Returns (width, height) tuple.
"""
return (self.width, self.height)
# end def
@property
def width (self):
"""Box width - read-write attribute."""
return self.__width
# end def
@width.setter
[docs] def width (self, value):
self.__width = self.normalize_dimension(value)
# end def
# end class
[docs]class WorldBoxBase3D (WorldBoxBase2D):
"""Base class for boxes in a 3D world."""
def __init__ (self, width=0, height=0, depth=0, **options):
"""Class constructor.
Parameter:
width -- box' width (default: 0).
height -- box' height (default: 0).
depth -- box' depth (default: 0).
options -- keyword arguments.
Supported keyword arguments:
-- none at this stage --
Notice: options are saved into self.options member attribute.
"""
try:
(self.width, self.height, self.depth) = width
except:
self.width = width
self.height = height
self.depth = depth
# end try
self.options = options
# end def
[docs] def center (self, query=None):
"""Build tuple of center dimensions along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xyz" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "xz", "yz",
"xyz", "yxz", "zxy", "zyx" and so on.
Will always return a tuple object, even for only one dimension
query e.g. self.center("x") --> (self.center_x, ).
"""
query = str(query or "xyz").lower()
_result = []
if "x" in query:
_result.append(self.center_x)
# end if
if "y" in query:
_result.append(self.center_y)
# end if
if "z" in query:
_result.append(self.center_z)
# end if
return tuple(_result)
# end def
@property
[docs] def center_xyz (self):
"""Tuple center of (X, Y, Z) dimensions - read-only attribute.
Current implementation merely returns a tuple of
(self.center_x, self.center_y, self.center_z).
"""
return (self.center_x, self.center_y, self.center_z)
# end def
@property
[docs] def center_z (self):
"""Center of Z dimension - read-only attribute.
Current implementation merely returns self.depth//2.
"""
return self.depth//2
# end def
@property
def depth (self):
"""Box depth - read-write attribute."""
return self.__depth
# end def
@depth.setter
[docs] def depth (self, value):
self.__depth = self.normalize_dimension(value)
# end def
@property
[docs] def size (self):
"""Box size - read-only attribute.
Returns (width, height, depth) tuple.
"""
return (self.width, self.height, self.depth)
# end def
[docs] def to_dict (self):
"""Dictionary representation of object attributes.
Returns dict(width=self.width, height=self.height,
depth=self.depth).
"""
return dict(
width=self.width, height=self.height, depth=self.depth
)
# end def
[docs] def to_tuple (self):
"""Tuple representation of object attributes.
Returns (width, height, depth) tuple.
"""
return (self.width, self.height, self.depth)
# end def
# end class
[docs]class WorldArea2D (WorldBoxBase2D):
"""Playground area for 2D world."""
def __init__ (self, width=0, height=0, **options):
"""Class constructor.
Parameter:
width -- area's width (default: 0).
height -- area's height (default: 0).
options -- keyword arguments.
Supported keyword arguments:
bounds or limits -- a couple of tuple coordinates or point
objects of diagonal vertices of area's bounding box e.g.
((0, 0), (800, 600)). Defaults to self.default_bounds if
None or omitted.
Notice: options are saved into self.options member attribute.
"""
super().__init__(width, height, **options)
self.bounds = options.get("bounds") or options.get("limits")
# end def
@property
def bounds (self):
"""Area's bounding box - read-write attribute."""
return self.__bounds
# end def
@bounds.setter
[docs] def bounds (self, value):
self.__bounds = self.bounds_type(
*(value or self.default_bounds)
)
# end def
@property
[docs] def bounds_type (self):
"""Preferred bounding box type - read-only attribute.
Current implementation returns WorldBoundingBox2D class type.
This can be redefined in subclasses to fit more specific needs.
"""
return WorldBoundingBox2D
# end def
@property
[docs] def default_bounds (self):
"""Default bounds for bounding box - read-only attribute."""
return ((0,) * len(self.size), self.size)
# end def
# end class
[docs]class WorldArea3D (WorldBoxBase3D, WorldArea2D):
"""Playground area for 3D world."""
def __init__ (self, width=0, height=0, depth=0, **options):
"""Class constructor.
Parameter:
width -- area's width (default: 0).
height -- area's height (default: 0).
depth -- area's depth (default: 0).
options -- keyword arguments.
Supported keyword arguments:
bounds or limits -- a couple of tuple coordinates or point
objects of diagonal vertices of area's bounding box e.g.
((0, 0, 0), (800, 600, 400)). Defaults to
self.default_bounds if None or omitted.
Notice: options are saved into self.options member attribute.
"""
WorldBoxBase3D.__init__(self, width, height, depth, **options)
self.bounds = options.get("bounds") or options.get("limits")
# end def
@property
[docs] def bounds_type (self):
"""Preferred bounding box type - read-only attribute.
Current implementation returns WorldBoundingBox3D class type.
This can be redefined in subclasses to fit more specific needs.
"""
return WorldBoundingBox3D
# end def
# end class
[docs]class WorldBoundingBox2D:
"""Bounding box for 2D world."""
def __getitem__ (self, key):
"""Make object iterable."""
return self.to_tuple()[key]
# end def
def __init__ (self, *points, **options):
"""Class constructor.
Parameter:
points -- variable list of tuple coordinates or point
objects for bounds calculations.
options -- keyword arguments.
Supported keyword arguments:
-- none at this stage --
Notice: options are saved into self.options member attribute.
"""
self.reset_bounds(*points)
self.options = options
# end def
def __repr__ (self):
"""Object string representation."""
return str(self.to_tuple())
# end def
[docs] def bounding_coords (self, *points):
"""Bounding coordinates.
Parameter:
points -- variable list of tuple coordinates or point
objects.
Returns a 2-tuple of tuple coordinates - minimum and maximum
coordinates along with given points.
"""
_coords = tuple(zip(*filter(None, points)))
return (tuple(map(min, _coords)), tuple(map(max, _coords)))
# end def
[docs] def bounding_corners (self, *points):
"""Bounding box corner points.
Parameter:
points -- variable list of tuple coordinates or point
objects.
returns a tuple list containing all point objects of bounding
box' corners (2D --> 4 corners, 3D --> 8 corners).
"""
_cmin, _cmax = self.bounding_coords(*points)
return tuple(
self.point_type(*coords)
for coords in itertools.product(*zip(_cmin, _cmax))
)
# end def
[docs] def bounding_points (self, *points):
"""Bounding box' min/max points.
Parameter:
points -- variable list of tuple coordinates or point
objects.
returns a 2-tuple list of bounding box' minimum coords point
object and maximum coords point object.
"""
points = self.bounding_coords(
*(points or (self.point1, self.point2))
)
return tuple(self.point_type(*coords) for coords in points)
# end def
[docs] def center (self, query=None):
"""Build tuple of center dimensions along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xy" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "yx", "y".
Will always return a tuple object, even for only one dimension
query e.g. self.center("x") --> (self.center_x, ).
"""
query = str(query or "xy").lower()
_result = []
if "x" in query:
_result.append(self.center_x)
# end if
if "y" in query:
_result.append(self.center_y)
# end if
return tuple(_result)
# end def
@property
[docs] def center_x (self):
"""Center of X dimension - read-only attribute.
Current implementation merely returns
(self.point1.x + self.point2.x)//2.
"""
return (self.point1.x + self.point2.x)//2
# end def
@property
[docs] def center_xy (self):
"""Tuple center of (X, Y) dimensions - read-only attribute.
Current implementation merely returns a tuple of
(self.center_x, self.center_y).
"""
return (self.center_x, self.center_y)
# end def
@property
[docs] def center_y (self):
"""Center of Y dimension - read-only attribute.
Current implementation merely returns
(self.point1.y + self.point2.y)//2.
"""
return (self.point1.y + self.point2.y)//2
# end def
[docs] def collided (self, *points):
"""Detect if any point is in current bounding box' bounds.
Parameter:
points -- variable list of tuple coordinates or point
objects.
Returns True if at least one of listed tuple coordinates (or
point object) is into current bounding box' bounds.
"""
return any(map(self.in_bounds, points))
# end def
@property
[docs] def corners (self):
"""Default bounding box corner points - read-only attribute.
See self.bounding_corners() docstring for more detail.
"""
return self.bounding_corners(self.point1, self.point2)
# end def
[docs] def entered (self, *points):
"""Evaluate bounding box' entered sides.
Parameter:
points -- variable list of tuple coordinates or point
objects.
Returns a tuple list of any current bounding box side name
('left', 'right', 'top', 'bottom') that has been entered by
given group of points.
"""
_bounds = []
_cmin, _cmax = self.bounding_coords(*points)
_p_min_in = self.truth_point(_cmin)
_p_max_in = self.truth_point(_cmax)
_any_in = _p_min_in + _p_max_in
if _any_in.y:
if _p_max_in.x:
_bounds.append("left")
# end if
if _p_min_in.x:
_bounds.append("right")
# end if
# end if
if _any_in.x:
if _p_min_in.y:
_bounds.append("top")
# end if
if _p_max_in.y:
_bounds.append("bottom")
# end if
# end if
return tuple(_bounds)
# end def
[docs] def exited (self, *points):
"""Evaluate bounding box' exited sides.
Parameter:
points -- variable list of tuple coordinates or point
objects.
Returns a tuple list of any current bounding box side name
('left', 'right', 'top', 'bottom') that has been exited by
given group of points.
"""
_bounds = []
_p_min, _p_max = self.bounding_points(*points)
_b_min, _b_max = self.bounding_points(self.point1, self.point2)
if _p_min.x <= _b_min.x:
_bounds.append("left")
# end if
if _p_max.x >= _b_max.x:
_bounds.append("right")
# end if
if _p_max.y >= _b_max.y:
_bounds.append("top")
# end if
if _p_min.y <= _b_min.y:
_bounds.append("bottom")
# end if
return tuple(_bounds)
# end def
@property
[docs] def height (self):
"""Bounding box' height - read-only attribute."""
return abs(self.point1.y - self.point2.y)
# end def
[docs] def in_bounds (self, point, *points):
"""Detect if point is in points' bounding box bounds.
Parameter:
point -- tuple coordinates or point object.
points -- variable list of tuple coordinates or point
objects.
Returns True if given point is included into the bounding box
of listed points (or into current bounding box, if points are
omitted).
"""
return all(self.truth_point(point, *points))
# end def
[docs] def overlapped (self, *points):
"""Detect if current bounding box is overlapped by points.
Parameter:
points -- variable list of tuple coordinates or point
objects.
Returns True if current bounding box is overlapped in any
manner by the bounding box of listed points.
"""
return bool(
self.in_bounds(self.point1, *points)
or self.in_bounds(self.point2, *points)
)
# end def
@property
def point1 (self):
"""Minimum coordinates point - read-write attribute."""
return self.__point1
# end def
@point1.setter
[docs] def point1 (self, value):
self.__point1 = self.point_type(*value)
# end def
@property
def point2 (self):
"""Maximum coordinates point - read-write attribute."""
return self.__point2
# end def
@point2.setter
[docs] def point2 (self, value):
self.__point2 = self.point_type(*value)
# end def
@property
[docs] def point_type (self):
"""Preferred point type - read-only attribute.
Current implementation returns WorldPoint2D class type.
This can be redefined in subclasses to fit more specific needs.
"""
return WorldPoint2D
# end def
[docs] def reset_bounds (self, *points):
"""Reset current bounding box' bounds.
Parameter:
points -- variable list of tuple coordinates or point
objects.
"""
self.point1, self.point2 = self.bounding_coords(*points)
# end def
@property
[docs] def size (self):
"""Bounding box size - read-only attribute.
Returns (width, height) tuple.
"""
return (self.width, self.height)
# end def
[docs] def to_dict (self):
"""Dictionary representation of object attributes.
Returns dict(point1=self.point1, point2=self.point2).
"""
return dict(point1=self.point1, point2=self.point2)
# end def
[docs] def to_flat_tuple (self):
"""Flat tuple representation.
All coordinates are linearized into a single tuple.
This converts a ((x1, y1), ..., (xn, yn)) tuple list of tuple
coordinates to a single (x1, y1, ..., xn, yn) tuple of flat
coordinates.
"""
return tuple(dim for point in self.to_tuple() for dim in point)
# end def
[docs] def to_tuple (self):
"""Tuple representation of object attributes.
Returns (self.point1, self.point2) tuple.
"""
return (self.point1, self.point2)
# end def
[docs] def truth_point (self, point, *points):
"""Boolean point representation of bounds detection.
Parameter:
point -- tuple coordinates or point object.
points -- variable list of tuple coordinates or point
objects.
Returns a point object where each dimension coordinate
represents the testing truth value of 'point' dimension
comprised between min and max of listed 'points' dimension.
In other words, we get something like::
Point(
int(minX(points) <= point.x <= maxX(points)),
int(minY(points) <= point.y <= maxY(points)),
int(minZ(points) <= point.z <= maxZ(points))
)
If 'points' are omitted, 'point' is compared to current
bounding box points (self.point1, self.point2).
"""
_coords = tuple(point)
_cmin, _cmax = self.bounding_coords(
*(points or (self.point1, self.point2))
)
return self.point_type(
*(
int(dmin <= dim <= dmax)
for dmin, dim, dmax in zip(_cmin, _coords, _cmax)
)
)
# end def
@property
[docs] def width (self):
"""Bounding box' width - read-only attribute."""
return abs(self.point1.x - self.point2.x)
# end def
# end class
[docs]class WorldBoundingBox3D (WorldBoundingBox2D):
"""Bounding box for 3D world."""
[docs] def center (self, query=None):
"""Build tuple of center dimensions along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xyz" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "xz", "yz",
"xyz", "yxz", "zxy", "zyx" and so on.
Will always return a tuple object, even for only one dimension
query e.g. self.center("x") --> (self.center_x, ).
"""
query = str(query or "xyz").lower()
_result = []
if "x" in query:
_result.append(self.center_x)
# end if
if "y" in query:
_result.append(self.center_y)
# end if
if "z" in query:
_result.append(self.center_z)
# end if
return tuple(_result)
# end def
@property
[docs] def center_xyz (self):
"""Tuple center of (X, Y, Z) dimensions - read-only attribute.
Current implementation merely returns a tuple of
(self.center_x, self.center_y, self.center_z).
"""
return (self.center_x, self.center_y, self.center_z)
# end def
@property
[docs] def center_z (self):
"""Center of Z dimension - read-only attribute.
Current implementation merely returns
(self.point1.z + self.point2.z)//2.
"""
return (self.point1.z + self.point2.z)//2
# end def
@property
[docs] def depth (self):
"""Bounding box' depth - read-only attribute."""
return abs(self.point1.z - self.point2.z)
# end def
[docs] def entered (self, *points):
"""Evaluate bounding box' entered sides.
Parameter:
points -- variable list of tuple coordinates or point
objects.
Returns a tuple list of any current bounding box side name
('left', 'right', 'top', 'bottom', 'front', 'rear') that has
been entered by given group of points.
"""
_bounds = []
_cmin, _cmax = self.bounding_coords(*points)
_p_min_in = self.truth_point(_cmin)
_p_max_in = self.truth_point(_cmax)
_any_in = _p_min_in + _p_max_in
if _any_in.y and _any_in.z:
if _p_max_in.x:
_bounds.append("left")
# end if
if _p_min_in.x:
_bounds.append("right")
# end if
# end if
if _any_in.x and _any_in.z:
if _p_min_in.y:
_bounds.append("top")
# end if
if _p_max_in.y:
_bounds.append("bottom")
# end if
# end if
if _any_in.x and _any_in.y:
if _p_max_in.z:
_bounds.append("front")
# end if
if _p_min_in.z:
_bounds.append("rear")
# end if
# end if
return tuple(_bounds)
# end def
[docs] def exited (self, *points):
"""Evaluate bounding box' exited sides.
Parameter:
points -- variable list of tuple coordinates or point
objects.
Returns a tuple list of any current bounding box side name
('left', 'right', 'top', 'bottom', 'front', 'rear') that has
been exited by given group of points.
"""
_bounds = []
_p_min, _p_max = self.bounding_points(*points)
_b_min, _b_max = self.bounding_points(self.point1, self.point2)
if _p_min.x <= _b_min.x:
_bounds.append("left")
# end if
if _p_max.x >= _b_max.x:
_bounds.append("right")
# end if
if _p_max.y >= _b_max.y:
_bounds.append("top")
# end if
if _p_min.y <= _b_min.y:
_bounds.append("bottom")
# end if
if _p_min.z <= _b_min.z:
_bounds.append("front")
# end if
if _p_max.z >= _b_max.z:
_bounds.append("rear")
# end if
return tuple(_bounds)
# end def
@property
[docs] def point_type (self):
"""Preferred point type - read-only attribute.
Current implementation returns WorldPoint3D class type.
This can be redefined in subclasses to fit more specific needs.
"""
return WorldPoint3D
# end def
@property
[docs] def size (self):
"""Bounding box size - read-only attribute.
Returns (width, height, depth) tuple.
"""
return (self.width, self.height, self.depth)
# end def
# end class
[docs]class WorldPoint2D:
"""Geometrical point for 2D world."""
def __add__ (self, other):
"""Analytical sum of two points (coordinates sum)."""
try:
return self.__class__(*map(operator.add, self, other))
except TypeError:
return NotImplemented
# end try
# end def
def __floordiv__ (self, other):
"""Floor division (//) of point's coordinates by a number."""
if isinstance(other, (int, float)):
return self.__class__(*(dim // other for dim in self))
else:
return NotImplemented
# end if
# end def
def __getitem__ (self, key):
"""Make object iterable."""
return self.to_tuple()[key]
# end def
def __init__ (self, x=0, y=0):
"""Class constructor.
Parameters:
x -- numeric dimension coordinate (default: 0).
y -- numeric dimension coordinate (default: 0).
"""
try:
(self.x, self.y) = x
except:
(self.x, self.y) = (x, y)
# end try
# end def
def __mul__ (self, other):
"""Multiplication of point's coordinates by a number."""
if isinstance(other, (int, float)):
return self.__class__(*(dim * other for dim in self))
else:
return NotImplemented
# end if
# end def
def __repr__ (self):
"""Object string representation."""
return str(self.to_tuple())
# end def
def __rmul__ (self, other):
"""Reverse multiplication - see __mul__() for more detail."""
return self.__mul__(other)
# end def
def __sub__ (self, other):
"""Analytical sub of two points (coordinates substraction)."""
try:
return self.__class__(*map(operator.sub, self, other))
except TypeError:
return NotImplemented
# end try
# end def
def __truediv__ (self, other):
"""True division (/) of point's coordinates by a number."""
if isinstance(other, (int, float)):
return self.__class__(*(dim / other for dim in self))
else:
return NotImplemented
# end if
# end def
[docs] def cdot (self, other):
"""Hadamard matrix product.
Examples::
Point(x, y).cdot(Point(a, b)) == Point(x*a, y*b)
Point(x, y, z).cdot(Point(a, b, c)) == Point(x*a, y*b, z*c)
"""
return self.__class__(*map(operator.mul, self, other))
# end def
[docs] def normalize_dimension (self, value):
"""Format value in order to fit with dimension constraints.
Parameter:
value -- should be a numeric value (int or float).
Will raise ValueError if 'value' is not a numeric value.
Returns 0 if bool(value) is False.
"""
if value:
if isinstance(value, (int, float)):
return value
else:
raise ValueError(
"Value must be of int or float type, not '{}'."
.format(value)
)
# end if
# end if
return 0
# end def
[docs] def to_dict (self):
"""Dictionary representation of object attributes.
Returns dict(x=self.x, y=self.y).
"""
return dict(x=self.x, y=self.y)
# end def
[docs] def to_tuple (self):
"""Tuple representation of object attributes.
Returns (x, y) tuple.
"""
return self.xy
# end def
@property
def x (self):
"""x dimension coordinate - read-write attribute."""
return self.__x
# end def
@x.setter
[docs] def x (self, value):
self.__x = self.normalize_dimension(value)
# end def
@property
[docs] def xy (self):
"""(x, y) coordinates - read-only attribute."""
return (self.x, self.y)
# end def
@property
def y (self):
"""y dimension coordinate - read-write attribute."""
return self.__y
# end def
@y.setter
[docs] def y (self, value):
self.__y = self.normalize_dimension(value)
# end def
# end class
[docs]class WorldPoint3D (WorldPoint2D):
"""Geometrical point for 3D world."""
def __init__ (self, x=0, y=0, z=0):
"""Class constructor.
Parameters:
x -- numeric dimension coordinate (default: 0).
y -- numeric dimension coordinate (default: 0).
z -- numeric dimension coordinate (default: 0).
"""
try:
(self.x, self.y, self.z) = x
except:
(self.x, self.y, self.z) = (x, y, z)
# end try
# end def
[docs] def to_dict (self):
"""Dictionary representation of object attributes.
Returns dict(x=self.x, y=self.y, z=self.z).
"""
return dict(x=self.x, y=self.y, z=self.z)
# end def
[docs] def to_tuple (self):
"""Tuple representation of object attributes.
Returns (x, y, z) tuple.
"""
return self.xyz
# end def
@property
[docs] def xyz (self):
"""(x, y, z) coordinates - read-only attribute."""
return (self.x, self.y, self.z)
# end def
@property
def z (self):
"""z dimension coordinate - read-write attribute."""
return self.__z
# end def
@z.setter
[docs] def z (self, value):
self.__z = self.normalize_dimension(value)
# end def
# end class
[docs]class WorldVector2D (WorldPoint2D):
"""Geometrical vector for 2D world."""
def __abs__ (self):
"""Vector norm."""
return sum(dim**2 for dim in self)**0.5
# end def
def _dim_query (self, query, prefix):
"""Protected method def - for internal use only."""
for _dim in query:
exec(
"self.{prefix}_{dimension}()"
.format(prefix=prefix, dimension=_dim)
)
# end for
return self
# end def
[docs] def abs (self, query=None):
"""Force vector coordinates to abs() along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xy" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "yx", "y".
Returns self.
"""
query = str(query or "xy").lower()
return self._dim_query(query, "abs")
# end def
[docs] def abs_x (self):
"""Force vector x coordinate to abs(x)."""
self.x = abs(self.x)
return self
# end def
[docs] def abs_y (self):
"""Force vector y coordinate to abs(y)."""
self.y = abs(self.y)
return self
# end def
[docs] def flip (self, query=None):
"""Flip (invert) vector coordinates along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xy" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "yx", "y".
Returns self.
"""
query = str(query or "xy").lower()
return self._dim_query(query, "flip")
# end def
[docs] def flip_x (self):
"""Flip vector x coordinate to -x."""
self.x = -self.x
return self
# end def
[docs] def flip_y (self):
"""Flip vector y coordinate to -y."""
self.y = -self.y
return self
# end def
[docs] def neg (self, query=None):
"""Negate vector coordinates along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xy" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "yx", "y".
Returns self.
"""
query = str(query or "xy").lower()
return self._dim_query(query, "neg")
# end def
[docs] def neg_x (self):
"""Force vector x coordinate to -abs(x)."""
self.x = -abs(self.x)
return self
# end def
[docs] def neg_y (self):
"""Force vector y coordinate to -abs(y)."""
self.y = -abs(self.y)
return self
# end def
# end class
[docs]class WorldVector3D (WorldPoint3D, WorldVector2D):
"""Geometrical vector for 3D world."""
def __init__ (self, x=0, y=0, z=0):
"""Class constructor.
Parameters:
x -- numeric dimension coordinate (default: 0).
y -- numeric dimension coordinate (default: 0).
z -- numeric dimension coordinate (default: 0).
"""
WorldPoint3D.__init__(self, x, y, z)
# end def
[docs] def abs (self, query=None):
"""Force vector coordinates to abs() along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xyz" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "xz", "yz",
"xyz", "yxz", "zxy", "zyx" and so on.
Returns self.
"""
return super().abs(query or "xyz")
# end def
[docs] def abs_z (self):
"""Force vector z coordinate to abs(z)."""
self.z = abs(self.z)
return self
# end def
[docs] def flip (self, query=None):
"""Flip (invert) vector coordinates along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xyz" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "xz", "yz",
"xyz", "yxz", "zxy", "zyx" and so on.
Returns self.
"""
return super().flip(query or "xyz")
# end def
[docs] def flip_z (self):
"""Flip vector z coordinate to -z."""
self.z = -self.z
return self
# end def
[docs] def neg (self, query=None):
"""Negate vector coordinates along with query.
Parameter:
query -- string of dims (default: None).
If None or omitted, 'query' will be replaced by "xyz" string of
chars.
Any combination of dims is allowed e.g. "x", "xy", "xz", "yz",
"xyz", "yxz", "zxy", "zyx" and so on.
Returns self.
"""
return super().neg(query or "xyz")
# end def
[docs] def neg_z (self):
"""Force vector z coordinate to -abs(z)."""
self.z = -abs(self.z)
return self
# end def
# end class