#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Shapes module.
Implements:
class WorldShapeBase2D -- mixin base class for 2D shape management.
class WorldShapeBase3D -- mixin base class for 3D shape management.
class WorldShapeBox3D -- shape specific class for managing 3D box
objects.
class WorldShapeCircle2D -- shape specific class for managing 2D
circle objects.
class WorldShapeImage2D -- shape specific class for managing 2D
image objects.
class WorldShapePolygon2D -- shape specific class for managing 2D
polygon objects.
class WorldShapePolyhedron3D -- shape specific class for managing
3D polyhedron objects.
class WorldShapeRectangle2D -- shape specific class for managing 2D
rectangle objects.
class WorldShapeSphere3D -- shape specific class for managing 3D
sphere objects.
"""
"""
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.
"""
try:
from . import geometry
except:
import geometry
# end try
[docs]class WorldShapeBase2D (geometry.WorldAnchorage2D):
"""Mixin base class for 2D shape management."""
_OFFSETS = (
(0, 0), (-1, 0), (0, -1), (-1, -1), (1, -1), (0, 1), (-1, 1),
(1, 1), (1, 0)
)
def __getitem__ (self, key):
"""Make object iterable."""
return self.to_tuple()[key]
# end def
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.
"""
super().__init__(**options)
self.ANCHOR_OFFSETS = dict(
zip(self.LEGAL_ANCHORS, self._OFFSETS)
)
# end def
def __repr__ (self):
"""Object string representation."""
return str(self.to_tuple())
# end def
@property
[docs] def bbox (self):
"""Bounding box - read-only attribute.
This bounding box is relative to self.origin anchorage point
and to self.anchor anchorage query.
"""
_center = self.center_point
_hs = self.half_size
return self.bbox_type(_center - _hs, _center + _hs)
# end def
@property
[docs] def bbox_type (self):
"""Preferred bounding box type - read-only attribute.
Current implementation returns geometry.WorldBoundingBox2D
class type.
This can be redefined in subclasses to fit more specific needs.
"""
return geometry.WorldBoundingBox2D
# end def
@property
[docs] def center_point (self):
"""Shape's center point - read-only attribute.
This point is relative to self.origin anchorage point and to
self.anchor anchorage query.
Uses self.unit_vector for local calculations.
"""
_offset = self.unit_vector.cdot(
self.ANCHOR_OFFSETS[self.anchor]
)
return (self.origin + _offset.cdot(self.half_size))
# end def
@property
[docs] def half_size (self):
"""Half dims of size - read-only attribute.
Returns half dims of size i.e. (width//2, height//2).
"""
_w, _h = self.size
return (_w//2, _h//2)
# end def
[docs] def points_to_tuple (self, *points):
"""Convert a list of given points to a tuple list of tuple
coordinates.
Example: [Point(1, 2), Point(3, 4), (5, 6), [7, 8]] will be
converted to ((1, 2), (3, 4), (5, 6), (7, 8)).
"""
return tuple(map(tuple, points))
# end def
@property
[docs] def size (self):
"""Shape's size - read-only attribute.
This attribute MUST be overridden in subclasses.
Will raise a NotImplementedError otherwise.
"""
# returns size of shape object
raise NotImplementedError
# end def
[docs] def to_tuple (self):
"""Tuple representation of shape object attributes.
This attribute MUST be overridden in subclasses.
Will raise a NotImplementedError otherwise.
"""
# tuple representation of shape object attributes
raise NotImplementedError
# end def
# end class
[docs]class WorldShapeBase3D (geometry.WorldAnchorage3D, WorldShapeBase2D):
"""Mixin base class for 3D shape management."""
_OFFSETS = tuple(
a + (b,)
for b in (-1, 0, 1)
for a in WorldShapeBase2D._OFFSETS
)
def __init__ (self, **options):
"""Class constructor.
Parameter:
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: MIDDLE + CENTER).
origin -- anchorage point of origin (default: None).
unit_vector -- local unit vector (default: (1, 1, 1)).
Notice: options are saved into self.options member attribute.
"""
geometry.WorldAnchorage3D.__init__(self, **options)
WorldShapeBase2D.__init__(self, **options)
# end def
@property
[docs] def bbox_type (self):
"""Preferred bounding box type - read-only attribute.
Current implementation returns geometry.WorldBoundingBox3D
class type.
This can be redefined in subclasses to fit more specific needs.
"""
return geometry.WorldBoundingBox3D
# end def
@property
[docs] def half_size (self):
"""Half dims of size - read-only attribute.
Returns half dims of size i.e. (width//2, height//2, depth//2).
"""
_w, _h, _d = self.size
return (_w//2, _h//2, _d//2)
# end def
# end class
[docs]class WorldShapeImage2D (WorldShapeBase2D):
"""Shape specific class for managing 2D image objects."""
def __init__ (self, origin, image, **options):
"""Class constructor.
Parameter:
origin -- tuple coordinates (x, y) or point object for
object's anchorage point of origin.
image -- image object.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: CENTER).
unit_vector -- local unit vector (default: (1, 1)).
Notice: options are saved into self.options member attribute.
"""
super().__init__(origin=origin, **options)
self.image = image
# end def
@property
[docs] def height (self):
"""Image height - read-only attribute."""
return self.image.height()
# end def
@property
[docs] def size (self):
"""Image size - read-only attribute.
Returns a tuple (width, height).
"""
return (self.width, self.height)
# end def
[docs] def to_tuple (self):
"""Tuple representation of shape object attributes."""
return self.bbox.to_tuple()
# end def
@property
[docs] def width (self):
"""Image width - read-only attribute."""
return self.image.width()
# end def
# end class
[docs]class WorldShapeCircle2D (WorldShapeBase2D):
"""Shape specific class for managing 2D circle objects."""
def __init__ (self, origin, radius, **options):
"""Class constructor.
Parameter:
origin -- tuple coordinates (x, y) or point object for
object's anchorage point of origin.
radius -- numeric value for circle's radius.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: CENTER).
unit_vector -- local unit vector (default: (1, 1)).
Notice: options are saved into self.options member attribute.
"""
super().__init__(origin=origin, **options)
self.radius = radius
# end def
@property
[docs] def size (self):
"""Circle size - read-only attribute.
Returns a tuple (width, height).
"""
_diameter = 2 * self.radius
# returns (width, height)
return (_diameter, _diameter)
# end def
[docs] def to_tuple (self):
"""Tuple representation of shape object attributes."""
return (self.center_point, self.radius)
# end def
# end class
[docs]class WorldShapeSphere3D (WorldShapeCircle2D, WorldShapeBase3D):
"""Shape specific class for managing 3D sphere objects."""
def __init__ (self, origin, radius, **options):
"""Class constructor.
Parameter:
origin -- tuple coordinates (x, y, z) or point object for
object's anchorage point of origin.
radius -- numeric value for sphere's radius.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: MIDDLE + CENTER).
unit_vector -- local unit vector (default: (1, 1, 1)).
Notice: options are saved into self.options member attribute.
"""
WorldShapeBase3D.__init__(self, **options)
WorldShapeCircle2D.__init__(self, origin, radius, **options)
# end def
@property
[docs] def size (self):
"""Sphere size - read-only attribute.
Returns a tuple (width, height, depth).
"""
_diameter = 2 * self.radius
# returns (width, height, depth)
return (_diameter, _diameter, _diameter)
# end def
# end class
[docs]class WorldShapePolygon2D (WorldShapeBase2D):
"""Shape specific class for managing 2D polygon objects."""
def __init__ (self, origin, *coords, **options):
"""Class constructor.
Parameters:
origin -- tuple coordinates (x, y) or point object for
object's anchorage point of origin.
coords -- variable list of tuple coordinates (x, y) or
point objects representing polygon's vertices.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: CENTER).
unit_vector -- local unit vector (default: (1, 1)).
Notice: options are saved into self.options member attribute.
"""
super().__init__(origin=origin, **options)
try:
self.coords = coords or options.get("coords")
except:
raise ValueError(
"Missing list of coordinates to build "
"a consistent polygon."
) from None
# end try
# end def
@property
def coords (self):
"""Polygon coordinates attribute (read-write).
Stores any series of point coordinates as a tuple list of tuple
coordinates (simplest form).
"""
return self.__coords
# end def
@coords.setter
[docs] def coords (self, values):
self.__coords = self.points_to_tuple(*values)
# end def
@property
[docs] def rel_coords (self):
"""Relative coordinates - read-only attribute.
These coordinates are relative to 'center_point' attribute and
to 'unit_vector' attribute.
"""
_u = self.unit_vector
_center = self.center_point
return self.points_to_tuple(
*(_center + _u.cdot(coords) for coords in self.coords)
)
# end def
@property
[docs] def rel_coords_flat (self):
"""Flat relative coordinates - read-only attribute.
All relative 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 coords in self.rel_coords for dim in coords
)
# end def
@property
[docs] def size (self):
"""Polygon size - read-only attribute.
Returns a tuple (width, height) of polygon's bounding box.
"""
return tuple(
max(dims) - min(dims) for dims in zip(*self.coords)
)
# end def
[docs] def to_tuple (self):
"""Tuple representation of shape object attributes."""
return self.rel_coords
# end def
# end class
[docs]class WorldShapePolyhedron3D (WorldShapePolygon2D, WorldShapeBase3D):
"""Shape specific class for managing 3D polyhedron objects."""
def __init__ (self, origin, *coords, **options):
"""Class constructor.
Parameters:
origin -- tuple coordinates (x, y, z) or point object for
object's anchorage point of origin.
coords -- variable list of tuple coordinates (x, y, z) or
point objects representing polyhedron's vertices.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: MIDDLE + CENTER).
unit_vector -- local unit vector (default: (1, 1, 1)).
Notice: options are saved into self.options member attribute.
"""
WorldShapeBase3D.__init__(self, **options)
WorldShapePolygon2D.__init__(self, origin, *coords, **options)
# end def
# end class
[docs]class WorldShapeRectangle2D (geometry.WorldBoxBase2D, WorldShapeBase2D):
"""Shape specific class for managing 2D rectangle objects."""
def __init__ (self, origin, width, height, **options):
"""Class constructor.
Parameters:
origin -- tuple coordinates (x, y) or point object for
object's anchorage point of origin.
width -- rectangle's width.
height -- rectangle's height.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: CENTER).
unit_vector -- local unit vector (default: (1, 1)).
Notice: options are saved into self.options member attribute.
"""
geometry.WorldBoxBase2D.__init__(
self, width, height, **options
)
WorldShapeBase2D.__init__(self, origin=origin, **options)
# end def
[docs] def to_tuple (self):
"""Tuple representation of shape object attributes."""
return self.bbox.to_tuple()
# end def
# end class
[docs]class WorldShapeBox3D (geometry.WorldBoxBase3D, WorldShapeBase3D):
"""Shape specific class for managing 3D box objects."""
def __init__ (self, origin, width, height, depth, **options):
"""Class constructor.
Parameters:
origin -- tuple coordinates (x, y, z) or point object for
object's anchorage point of origin.
width -- box' width.
height -- box' height.
depth -- box' depth.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query (default: MIDDLE + CENTER).
unit_vector -- local unit vector (default: (1, 1, 1)).
Notice: options are saved into self.options member attribute.
"""
geometry.WorldBoxBase3D.__init__(
self, width, height, depth, **options
)
WorldShapeBase3D.__init__(self, origin=origin, **options)
# end def
[docs] def to_tuple (self):
"""Tuple representation of shape object attributes."""
return self.bbox.to_tuple()
# end def
# end class
[docs]def run_demo ():
import tkinter as tk
_root = tk.Tk()
_root.title("CoolWorld.shapes tkinter demo")
_root.resizable(0, 0)
_canvas = tk.Canvas(_root, width=400, height=250, bg="black")
_canvas.unit_vector = (1, -1)
def show_shape (shape, shape_factory):
_cr = (2, 2)
for _a in shape.LEGAL_ANCHORS:
_canvas.create_text(
tuple(shape.origin + (0, -30)),
text=_a.upper(), fill="white"
)
shape.anchor = _a
shape_factory(shape)
_origin = shape.origin
_canvas.create_oval(
shape.points_to_tuple(_origin - _cr, _origin + _cr),
fill="cyan"
)
shape.origin += (40, 0)
# end for
# end def
_shape = WorldShapeCircle2D(
origin=(35, 50), radius=10, unit_vector=_canvas.unit_vector,
)
_factory = (
lambda shape:
_canvas.create_oval(
shape.bbox.to_flat_tuple(), fill="yellow"
)
)
show_shape(_shape, _factory)
_shape = WorldShapeRectangle2D(
origin=(35, 130), width=20, height=10,
unit_vector=_canvas.unit_vector,
)
_factory = (
lambda shape:
_canvas.create_rectangle(
shape.bbox.to_flat_tuple(), fill="orange"
)
)
show_shape(_shape, _factory)
_shape = WorldShapePolygon2D(
origin=(35, 210),
coords=(
(9.51, 3.09), (-9.51, 3.09), (5.88, -8.09), (0.0, 10.0),
(-5.88, -8.09)
),
unit_vector=_canvas.unit_vector,
)
_factory = (
lambda shape:
_canvas.create_polygon(shape.rel_coords, fill="magenta")
)
show_shape(_shape, _factory)
_canvas.pack()
_root.mainloop()
# end def
if __name__ == "__main__":
run_demo()
# end if