Source code for cockatoo.utilities

"""
.. currentmodule:: cockatoo.utilities

.. autosummary::
    :nosignatures:

    blend_colors
    break_polyline
    map_values_as_colors
    tween_planes
    is_ccw_xy
    resolve_order_by_backtracking
"""

# PYTHON STANDARD LIBRARY IMPORTS ---------------------------------------------
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from collections import deque
from itertools import tee
from math import cos
from math import pi
from math import sqrt

# DUNDER ----------------------------------------------------------------------
__all__ = [
    "blend_colors",
    "break_polyline",
    "map_values_as_colors",
    "tween_planes",
    "is_ccw_xy",
    "resolve_order_by_backtracking",
    "pairwise"
]

# LOCAL MODULE IMPORTS --------------------------------------------------------
from cockatoo.environment import RHINOINSIDE
from cockatoo.exception import SystemNotPresentError

# RHINO IMPORTS ---------------------------------------------------------------
if RHINOINSIDE:
    import rhinoinside
    rhinoinside.load()
    from Rhino.Display import ColorHSL as RhinoColorHSL
    from Rhino.Geometry import Polyline as RhinoPolyline
    from Rhino.Geometry import Quaternion as RhinoQuaternion
    from Rhino.Geometry import Vector3d as RhinoVector3d
else:
    from Rhino.Display import ColorHSL as RhinoColorHSL
    from Rhino.Geometry import Polyline as RhinoPolyline
    from Rhino.Geometry import Quaternion as RhinoQuaternion
    from Rhino.Geometry import Vector3d as RhinoVector3d

# RHINO GEOMETRY --------------------------------------------------------------


[docs]def break_polyline(polyline, break_angle, as_crv=False): """ Breaks a polyline at kinks based on a specified angle. Will move the seam of closed polylines to the first kink discovered. Parameters ---------- polyline : :obj:`Rhino.Geometry.Polyline` Polyline to break apart at angles. break_angle : float The angle at which to break apart the polyline (in radians). as_crv : bool, optional If ``True``, will return a :obj:`Rhino.Geometry.PolylineCurve` object. Defaults to ``False``. Returns ------- polyline_segments : list of :obj:`Rhino.Geometry.Polyline` A list of the broken segments as Polylines if ``as_crv`` is ``False``. polyline_segments: list of :obj:`Rhino.Geometry.PolylineCurve` A list of the broken segments as PolylineCurves if ``as_crv`` is ``True``. """ # get all the polyline segments segments = deque(polyline.GetSegments()) # check if polyline in closed if polyline.IsClosed: closedSeamAtKink = False else: closedSeamAtKink = True # initialize containers plcs = [] pl = RhinoPolyline() # process all segments while len(segments) > 0: # if there is only one segment left, add the endpoint to the new pl if len(segments) == 1: ln = segments.popleft() pl.Add(ln.To) plcs.append(pl) break # get unitized directions of this and next segment thisdir = segments[0].Direction nextdir = segments[1].Direction thisdir.Unitize() nextdir.Unitize() # compute angle vdp = thisdir * nextdir angle = cos(vdp / (thisdir.Length * nextdir.Length)) angle = RhinoVector3d.VectorAngle(thisdir, nextdir) # check angles and execute breaks if angle >= break_angle: if not closedSeamAtKink: segments.rotate(-1) pl.Add(segments.popleft().From) closedSeamAtKink = True elif closedSeamAtKink: ln = segments.popleft() pl.Add(ln.From) pl.Add(ln.To) plcs.append(pl) pl = RhinoPolyline() else: if not closedSeamAtKink: segments.rotate(-1) else: pl.Add(segments.popleft().From) if as_crv: return [pline.ToPolylineCurve() for pline in plcs] else: return plcs
[docs]def tween_planes(pa, pb, t): """ Tweens between two planes using quaternion rotation. Based on code by Chris Hanley. [19]_ Parameters ---------- pa : :obj:`Rhino.Geometry.Plane` The start plane for the tween. pb : :obj:`Rhino.Geometry.Plane` The end plane for the tween. t : float The parameter for the tweened plane. 0.5 will result in the average between the two input planes. Returns ------- tweened_plane : :obj:`Rhino.Geometry.Plane` The plane between ``pa`` and ``pb`` at parameter ``t``. Raises ------ SystemNotPresentError If the ``System`` module cannot be imported. References ---------- .. [19] *Average between two planes* See: `Thread on discourse.mcneel.com <https://discourse.mcneel.com/ t/average-between-two-planes/71363/10>`_ """ # handle dotnet dependency in a nice way try: from clr import Reference from System import Double except ImportError: errMsg = "Could not import System. This function cannot execute!" raise SystemNotPresentError(errMsg) # create the quternion rotation between the two input planes Q = RhinoQuaternion.Rotation(pa, pb) # prepare out parameters qAngle = Reference[Double]() qAxis = Reference[RhinoVector3d]() # get the rotation of the quaternion Q.GetRotation(qAngle, qAxis) axis = RhinoVector3d(qAxis.X, qAxis.Y, qAxis.Z) angle = float(qAngle) - 2 * pi if float(qAngle) > pi else float(qAngle) out_plane = pa.Clone() out_plane.Rotate(t * angle, axis, out_plane.Origin) translation = RhinoVector3d(pb.Origin - pa.Origin) out_plane.Translate(translation * t) return out_plane
# RHINO DISPLAY ---------------------------------------------------------------
[docs]def blend_colors(col_a, col_b, t=0.5): """ Blend between two colors using the square root of photon flux. For more info see *Algorithm for additive color mixing for RGB values* [18]_. Parameters ---------- col_a : sequence of :obj:`int` Sequence of (R, G, B) that defines the color value. col_b : sequence of :obj:`int` Sequence of (R, G, B) that defines the color value. t : float, optional Parameter to define the blend location between the two colors. Defaults to ``0.5``. Returns ------- color : tuple 3-tuple of (R, G, B) that defines the new color. References ---------- .. [18] *Algorithm for additive color mixing for RGB values* See: `Thread on stackoverflow <https://stackoverflow.com/a/ 29321264>`_ """ # sanitize the blending parameter if t < 0: t = 0 elif t > 1: t = 1 # unpack colors in r, g, b values a_r, a_g, a_b = col_a b_r, b_g, b_b = col_b # compute the new rgb values for the blended color new_r = sqrt((1 - t) * a_r ** 2 + t * b_r ** 2) new_g = sqrt((1 - t) * a_g ** 2 + t * b_g ** 2) new_b = sqrt((1 - t) * a_b ** 2 + t * b_b ** 2) # return the new color tuple return (new_r, new_g, new_b)
[docs]def map_values_as_colors(values, src_min, src_max, target_min=0.0, target_max=0.7): """ Make a list of HSL colors where the values are mapped onto a targetMin-targetMax hue domain. Meaning that low values will be red, medium values green and large values blue if target_min is ``0.0`` and target_max is ``0.7``. Parameters ---------- values : list List of values to map as colors. src_min : float Lower bounds of the value domain. src_max : float Upper bounds of the value domain. target_min : float, optional Lower bounds of the target (color) domain. Defaults to ``0``. target_max : float, optional Upper bounds of the target (color) domain. Defaults to ``0.7`` . Returns ------- colors : list List of RGB colors corresponding to the input values. Notes ----- Based on code by Anders Holden Deleuran. Code was only changed in regards of defaults and names. For more info see *mapValuesAsColors.py* [10]_ . References ---------- .. [10] Deleuran, Anders Holden *mapValuesAsColors.py* See: `mapValuesAsColors.py gist <https://gist.github.com/ AndersDeleuran/82fa2a8a69ec10ac68176e1b848fdeea>`_ """ # remap numbers into new numeric domain remapped_values = [] for v in values: if src_max - src_min > 0: rv = ((v - src_min) / (src_max - src_min)) \ * (target_max - target_min) \ + target_min else: rv = (target_min + target_max) / 2 remapped_values.append(rv) # make rgb colors and return colors = [] for v in remapped_values: c = RhinoColorHSL(v, 1.0, 0.5).ToArgbColor() colors.append(c) return colors
# FUNCTIONAL GRAPH UTILITIES -------------------------------------------------- def _backtrack_node(G, node, pos, ordered_stack): """ Backtracks a node until no new predecessors are found and inserts the node and all dependencies in order into the ordered stack list. """ # check the node for dependencies dependencies = [pred for pred in G.predecessors_iter(node) if pred not in ordered_stack] # if node has no dependencies that are not already in the stack, # insert into the ordered stack of nodes and increment the pointer if not dependencies: if node not in ordered_stack: ordered_stack.insert(pos, node) pos += 1 return pos, ordered_stack else: # if node has dependencies, build a local stack of dependencies dependencies = deque(dependencies) # backtrack all dependencies while len(dependencies) > 0: dependency = dependencies.pop() pos, ordered_stack = _backtrack_node(G, dependency, pos, ordered_stack) # after all its dependencies are solved, insert the # dependent node at the current pointer position if dependency not in ordered_stack: ordered_stack.insert(pos, dependency) pos += 1 # after dependencies and sub-dependencies are solved, insert the node ordered_stack.insert(pos, node) pos += 1 # return the current pos and the filled ordered stack return pos, ordered_stack
[docs]def resolve_order_by_backtracking(G): """ Resolve topological order of a networkx DiGraph through backtracking of all nodes in the graph. Nodes are only inserted into the output list if all their dependencies (predecessor nodes) are already inside the output list, otherwise the algorithm will first resolve all open dependencies. Parameters ---------- G : :class:`networkx.Graph` The graph on which to perform topological sorting. Returns ------- ordered_nodes : list List of hashable node identifiers. Raises ------ ValueError If the input graph is not directed. Warning ------- For this to work, the input gaph must be a DAG (directed acyclic graph). For more info,see [11]_ and [12]_. References ---------- .. [11] Directed acyclic graph on Wikipedia. See: `Directed acyclic graph <https://en.wikipedia.org/wiki/ Directed_acyclic_graph>`_ .. [12] Topological sorting on Wikipedia. See: `Topological sorting <https://en.wikipedia.org/wiki/ Topological_sorting>`_ """ # raise if graph is not directed if not G.is_directed(): raise ValueError("This works only on directed graphs!") # stack is every node that has not been inserted yet stack = deque(G.nodes()) # pos is the current pointer for insertion pos = 0 # ordered stack is the target list for insertion ordered_stack = [] # backtrack the whole stack while len(stack) > 0: # pop an arbitrary node from the stack current_node = stack.pop() # backtrack that node and resolve all its dependencies pos, ordered_stack = _backtrack_node(G, current_node, pos, ordered_stack) # return the ordered stack return ordered_stack
# PURE PYTHON GEOMETRY --------------------------------------------------------
[docs]def is_ccw_xy(a, b, c, colinear=False): """ Determine if c is on the left of ab when looking from a to b, and assuming that all points lie in the XY plane. Parameters ---------- a : sequence of float XY(Z) coordinates of the base point. b : sequence of float XY(Z) coordinates of the first end point. c : sequence of float XY(Z) coordinates of the second end point. colinear : bool, optional Allow points to be colinear. Default is ``False``. Returns ------- bool ``True`` if ccw. ``False`` otherwise. Notes ----- Based on an implementation inside the COMPAS framework. For more info, see [14]_ and [15]_. References ---------- .. [14] Van Mele, Tom et al. *COMPAS: A framework for computational research in architecture and structures*. See: `is_ccw_xy() inside COMPAS <https://github.com/compas-dev/ compas/blob/e313502995b0dd86d460f86e622cafc0e29d1b75/src/compas/ geometry/_core/queries.py#L61>`_ .. [15] Marsh, C. *Computational Geometry in Python: From Theory to Application*. See: `Computational Geometry in Python <https://www.toptal.com/ python/ computational-geometry-in-python-from-theory-to-implementation>`_ Examples -------- >>> print(is_ccw_xy([0,0,0], [0,1,0], [-1, 0, 0])) True >>> print(is_ccw_xy([0,0,0], [0,1,0], [+1, 0, 0])) False >>> print(is_ccw_xy([0,0,0], [1,0,0], [2,0,0])) False >>> print(is_ccw_xy([0,0,0], [1,0,0], [2,0,0], True)) True """ ab_x = b[0] - a[0] ab_y = b[1] - a[1] ac_x = c[0] - a[0] ac_y = c[1] - a[1] if colinear: return ab_x * ac_y - ab_y * ac_x >= 0 return ab_x * ac_y - ab_y * ac_x > 0
# PYTHON HELPERS AND UTILITIES ------------------------------------------------
[docs]def pairwise(iterable): """ Returns the data of iterable in pairs (2-tuples). Parameters ---------- iterable : iterable An iterable sequence of items. Yields ------ tuple Two items per iteration, if there are at least two items in the iterable. Examples -------- >>> print(pairwise(range(4))): ... [(0, 1), (1, 2), (2, 3)] Notes ----- For more info see [16]_ . References ---------- .. [16] Python itertools Recipes See: `Python itertools Recipes <https://docs.python.org/2.7/ library/itertools.html#recipes>`_ """ a, b = tee(iterable) next(b, None) return zip(a, b)