Source code for tangible.ast

# -*- coding: utf-8 -*-
"""
AST module.

This module contains the building blocks for an abstract syntax tree (AST)
representation of 3D objects. It is implemented using namedtuples.

"""
from __future__ import print_function, division, absolute_import, unicode_literals

from itertools import chain

__VERSION__ = '1'


### Base class for AST types ###

class AST(object):
    """Base class for AST objects."""

    def __eq__(self, other):
        """This method override ensures that two objects are considered equal
        when their attributes match. Object identity is irrelevant."""
        return isinstance(other, self.__class__) \
            and self.__dict__ == other.__dict__

    def __ne__(self, other):
        """Inverse of ``__eq__``."""
        return not self.__eq__(other)

    def __repr__(self):
        name = self.__class__.__name__
        return '<AST/{0}: {1}>'.format(name, id(self))


### 2D shapes ###

[docs]class Circle(AST): """A circle 2D shape.""" def __init__(self, radius): """ :param radius: The radius of the circle. :type radius: int or float :raises: ValueError if validation fails. """ if radius <= 0: raise ValueError('Radius of a circle must be > 0.') self.radius = radius
[docs]class CircleSector(Circle): """A circle sector (pizza slice).""" def __init__(self, radius, angle): """ :param radius: The radius of the circle. :type radius: int or float :param angle: The central angle in degrees. :type angle: int or float :raises: ValueError if validation fails. """ super(CircleSector, self).__init__(radius) if angle <= 0: raise ValueError('Angle must be > 0.') if angle > 360: raise ValueError('Angle must be between 0 and 360.') self.angle = angle
[docs]class Rectangle(AST): """A rectangle 2D shape.""" def __init__(self, width, height): """ :param width: Width of the rectangle. :type width: int or float :param height: Height of the rectangle. :type height: int or float :raises: ValueError if validation fails. """ if width <= 0: raise ValueError('Width must be > 0.') if height <= 0: raise ValueError('Height must be > 0.') self.width = width self.height = height
[docs]class Polygon(AST): """A polygon 2D shape.""" def __init__(self, points): """ :param points: List of coordinates. Order of points is significant. The shape must be closed, meaning that the first and the last coordinate must be the same. :type points: list of 2-tuples :raises: ValueError if validation fails. """ if points[0] != points[-1]: raise ValueError('The shape must be closed, meaning that the first ' 'and the last coordinate must be the same.') if len(points) < 4: raise ValueError('A polygon consists of at least 3 points.') self.points = points # 3D shapes
[docs]class Cube(AST): """A cube 3D shape.""" def __init__(self, width, height, depth): """ :param width: Width of the cube. :type width: int or float :param height: Height of the cube. :type height: int or float :param depth: Depth of the cube. :type depth: int or float :raises: ValueError if validation fails. """ if width <= 0: raise ValueError('Width must be > 0.') if height <= 0: raise ValueError('Height must be > 0.') if depth <= 0: raise ValueError('Depth must be > 0.') self.width = width self.height = height self.depth = depth
[docs]class Sphere(AST): """A sphere 3D shape.""" def __init__(self, radius): """ :param radius: The radius of the sphere. :type radius: int or float :raises: ValueError if validation fails. """ if radius <= 0: raise ValueError('Radius of a sphere must be > 0.') self.radius = radius
[docs]class Cylinder(AST): """A cylinder 3D shape.""" def __init__(self, height, radius1, radius2): """ :param height: The height of the cylinder. :type height: int or float :param radius1: The bottom radius of the cylinder. :type radius1: int or float :param radius2: The top radius of the cylinder. :type radius2: int or float :raises: ValueError if validation fails. """ if height <= 0: raise ValueError('Height of a cylinder must be > 0.') if radius1 <= 0: raise ValueError('Bottom radius (radius1) of a cylinder must be > 0.') if radius2 <= 0: raise ValueError('Top radius (radius2) of a cylinder must be > 0.') self.height = height self.radius1 = radius1 self.radius2 = radius2
[docs]class Polyhedron(AST): """A polyhedron 3D shape. Supports both triangles and quads. Triangles and quads can also be mixed.""" def __init__(self, points, triangles=[], quads=[]): """ :param points: List of points. :type points: list of 3-tuples :param triangles: Triangles formed by a 3-tuple of point indexes (e.g. ``(0, 1, 3)``). When looking at the triangle from outside, the points must be in clockwise order. Default: ``[]``. :type triangles: list of 3-tuples :param quads: Rectangles formed by a 4-tuple of point indexes (e.g. ``(0, 1, 3, 4)``). When looking at the rectangle from outside, the points must be in clockwise order. Default: ``[]``. :type quads: list of 4-tuples :raises: ValueError if validation fails. """ if len(points) < 4: raise ValueError('There must be at least 4 points in a polyhedron.') if set(map(len, points)) != set([3]): raise ValueError('Invalid point tuples (must be 3-tuples).') if not (triangles or quads): raise ValueError('Either triangles or quads must be specified.') if triangles: if set(map(len, triangles)) != set([3]): raise ValueError('Invalid triangle tuples (must be 3-tuples).') if quads: if set(map(len, quads)) != set([4]): raise ValueError('Invalid quad tuples (must be 4-tuples).') max_value = max(chain(*(triangles + quads))) min_value = min(chain(*(triangles + quads))) if max_value >= len(points): raise ValueError('Invalid point index: {}'.format(max_value)) if min_value < 0: raise ValueError('Invalid point index: {}'.format(min_value)) self.points = points self.triangles = triangles self.quads = quads ### Transformations ###
[docs]class Translate(AST): """A translate transformation.""" def __init__(self, x, y, z, item): """ :param x: Translation on the X axis. :type x: int or float :param y: Translation on the Y axis. :type y: int or float :param z: Translation on the Z axis. :type z: int or float :param item: An AST object. :type item: tangible.ast.AST :raises: ValueError if validation fails """ if not item: raise ValueError('Item is required.') if not isinstance(item, AST): raise ValueError('Item must be an AST type.') self.x = x self.y = y self.z = z self.item = item
[docs]class Rotate(AST): """A rotate transformation.""" def __init__(self, degrees, vector, item): """ :param degrees: Number of degrees to rotate. :type degrees: int or float :param vector: The axes to rotate around. When a rotation is specified for multiple axes then the rotation is applied in the following order: x, y, z. As an example, a vector of [1,1,0] will cause the object to be first rotated around the x axis, and then around the y axis. :type y: 3-tuple :param item: An AST object. :type item: tangible.ast.AST :raises: ValueError if validation fails """ if not item: raise ValueError('Item is required.') if not isinstance(item, AST): raise ValueError('Item must be an AST type.') if not len(vector) == 3: raise ValueError('Invalid vector (must be a 3-tuple).') if not any(vector): raise ValueError('Invalid vector (must contain at least one `1` value).') if set(vector) != set([0, 1]): raise ValueError('Invalid vector (must consist of `0` and `1` values).') self.degrees = degrees self.vector = vector self.item = item
[docs]class Scale(AST): """A scale transformation.""" def __init__(self, x, y, z, item): """ The x, y and z attributes are multiplicators of the corresponding dimensions. E.g. to double the height of an object, you'd use ``1, 1, 2`` as x, y and z values. :param x: X axis multiplicator. :type x: int or float :param y: Y axis multiplicator. :type y: int or float :param z: Z axis multiplicator. :type z: int or float :param item: An AST object. :type item: tangible.ast.AST :raises: ValueError if validation fails """ if not item: raise ValueError('Item is required.') if not isinstance(item, AST): raise ValueError('Item must be an AST type.') if 0 in [x, y, z]: raise ValueError('Values of 0 are not allowed in a scale transformation.') self.x = x self.y = y self.z = z self.item = item
[docs]class Mirror(AST): """A mirror transformation.""" def __init__(self, vector, item): """ Mirror the child element on a plane through the origin. :param vector: Normal vector describing the plane intersecting the origin through which to mirror the object. :type vector: 3-tuple :param item: An AST object. :type item: tangible.ast.AST :raises: ValueError if validation fails """ if not item: raise ValueError('Item is required.') if not isinstance(item, AST): raise ValueError('Item must be an AST type.') if not len(vector) == 3: raise ValueError('Invalid vector (must be a 3-tuple).') if not any(vector): raise ValueError('Invalid vector (must contain at least one non-zero value).') self.vector = tuple(vector) self.item = item ### Boolean operations ###
class _BooleanOperation(AST): """Base class for boolean operations that only take the ``items`` argument.""" def __init__(self, items): """ :param items: List of AST objects. :type items: list :raises: ValueError if validation fails """ if not items: raise ValueError('Items are required.') if not hasattr(items, '__iter__'): raise ValueError('Items must be iterable.') if len(items) < 2: raise ValueError('Union must contain at least 2 items.') if not all(map(lambda x: isinstance(x, AST), items)): raise ValueError('All items must be AST types.') self.items = items
[docs]class Union(_BooleanOperation): """A union operation.""" pass
[docs]class Difference(_BooleanOperation): """A difference operation.""" pass
[docs]class Intersection(_BooleanOperation): """A intersection operation.""" pass ### Extrusions ###
[docs]class LinearExtrusion(AST): """A linear extrusion along the z axis.""" def __init__(self, height, item, twist=0): """ :param height: The height of the extrusion. :type height: int or float :param item: An AST object. :type item: tangible.ast.AST :param twist: How many degrees to twist the object around the z axis. :type twist: int or float """ if not item: raise ValueError('Item is required.') if not isinstance(item, AST): raise ValueError('Item must be an AST type.') self.height = height self.item = item self.twist = twist
[docs]class RotateExtrusion(AST): """A rotational extrusion around the z axis.""" def __init__(self, item): """ :param item: An AST object. :type item: tangible.ast.AST """ if not item: raise ValueError('Item is required.') if not isinstance(item, AST): raise ValueError('Item must be an AST type.') self.item = item