Source code for tangible.utils

# -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import, unicode_literals

from itertools import tee, izip

from .ast import Circle, Rectangle, Polygon, Cylinder, Polyhedron, Union, Rotate, Translate


[docs]def pairwise(iterable): """Iterate over an iterable in pairs. This is an implementation of a moving window over an iterable with 2 items. Each group in the resulting list contains 2 items. This means that the original iterable needs to contain at least 2 items, otherwise this function will return an empty list. Example:: [1, 2, 3, 4] -> [(1, 2), (2, 3), (3, 4)] :param iterable: An iterable containing at least 2 items. :type iterable: Any iterable type (e.g. a list or a tuple). :returns: A generator returning pairwise items. :rtype: :class:`itertools.izip` """ a, b = tee(iterable) next(b, None) return izip(a, b)
[docs]def reduceby(iterable, keyfunc, reducefunc, init): """Combination of ``itertools.groupby()`` and ``reduce()``. This generator iterates over the iterable. The values are reduced using ``reducefunc`` and ``init`` as long as ``keyfunc(item)`` returns the same value. A possible use case would be to aggregate website visits and to group them by month. The corresponding SQL statement would be:: SELECT SUM(visit_count) FROM visits GROUP BY month; Example:: >>> keyfunc = lambda x: x % 2 == 0 >>> reducefunc = lambda x, y: x + y >>> values = [1, 3, 5, 6, 8, 11] >>> groups = utils.reduceby(values, keyfunc, reducefunc, 0) >>> groups <generator object reduceby at 0xedc5a0> >>> list(groups) [9, 14, 11] :param iterable: An iterable to reduce. The iterable should be presorted. :param keyfunc: A key function. It should return the same value for all items belonging to the same group. :param reducefunc: The reduce function. :param init: The initial value for the reduction. :returns: A generator returning the reduced groups. :rtype: generator """ first = True oldkey = None accum_value = init for i in iter(iterable): key = keyfunc(i) if first: oldkey = key first = False elif key != oldkey: yield accum_value accum_value = init oldkey = key accum_value = reducefunc(accum_value, i) yield accum_value
[docs]def connect_2d_shapes(shapes, layer_distance, orientation): """Convert a list of 2D shapes to a 3D shape. Take a list of 2D shapes and create a 3D shape from it. Each layer is separated by the specified layer distance. :param shapes: List of shapes. :type shapes: Each shape in the list should be an AST object. :param layer_distance: The distance between two layers. :type layer_distance: int or float :param orientation: Either 'horizontal' or 'vertical' :type orientation: str or unicode :returns: :class:`ast.Union` """ assert orientation in ['horizontal', 'vertical'], \ '`orientation` argument must be either "horizontal" or "vertical".' layers = [] istype = lambda inst, cls: inst.__class__ is cls for i, (first, second) in enumerate(pairwise(shapes)): layer = None # Validate type if type(first) != type(second): raise NotImplementedError('Joining different shape types is not currently supported.') # Circle # Implemented by joining cylinders. if istype(first, Circle): r1, r2 = first.radius, second.radius layer = Cylinder(height=layer_distance, radius1=r1, radius2=r2) # Rectangle # Implemented by joining polyhedra. elif istype(first, Rectangle): w1, h1 = first.width, first.height w2, h2 = second.width, second.height def get_layer_points(x, y, z): return [ [x / 2, y / 2, z], [-x / 2, y / 2, z], [-x / 2, -y / 2, z], [x / 2, -y / 2, z], ] points = [] points.extend(get_layer_points(w1, h1, 0)) points.extend(get_layer_points(w2, h2, layer_distance)) quads = [ # Bottom [0, 1, 2, 3], # Top [4, 7, 6, 5], # Sides [0, 4, 5, 1], [1, 5, 6, 2], [2, 6, 7, 3], [3, 7, 4, 0], ] layer = Polyhedron(points=points, quads=quads) # Polygon # Implemented by joining polyhedra. elif istype(first, Polygon): if len(first.points) != len(second.points): raise ValueError('All polygons need to have the same number of points.') vertice_count = len(first.points) - 1 points = [] for point in first.points[:-1]: points.append(list(point) + [0]) for point in second.points[:-1]: points.append(list(point) + [layer_distance]) triangles = [] quads = [] for j in xrange(vertice_count): # Sides quads.append([ (j + 1) % vertice_count, # lower right j, # lower left vertice_count + j, # upper left vertice_count + (j + 1) % vertice_count # upper right ]) if j >= 2 and j < vertice_count: # Bottom triangles.append([0, j - 1, j]) # Top triangles.append([vertice_count + j, vertice_count + j - 1, vertice_count]) layer = Polyhedron(points=points, quads=quads, triangles=triangles) else: raise ValueError('Unsupported shape: {!r}'.format(first)) layers.append(Translate(0, 0, i * layer_distance, item=layer)) union = Union(items=layers) if orientation == 'horizontal': return Rotate(degrees=90, vector=[0, 1, 0], item=union) return union
def _quads_to_triangles(quads): """Convert a list of quads to a list of triangles. :param quads: The list of quads. :type quads: list of 4-tuples :returns: List of triangles. :rtype: list of 3-tuples """ triangles = [] for quad in quads: triangles.append((quad[0], quad[1], quad[2])) triangles.append((quad[0], quad[2], quad[3])) return triangles def _ensure_list_of_lists(data): """Ensure the data object is a list of lists. If it doesn't contain lists or tuples, wrap it in a list. :param data: The dataset. :type data: list or tuple :returns: Processed data. :rtype: list of lists :raises: ValueError if data is not a sequence type. """ if not hasattr(data, '__iter__'): raise ValueError('Data must be a sequence type (e.g. a list)') if not data: return [[]] if hasattr(data[0], '__iter__'): return data return [data]