def draw_collection(collection, names=None, colors=None, layer=None, clear=False, add_to_group=False, group_name=None): """Draw a collection of circles. Parameters ---------- collection : list of :class:`compas.geometry.Circle` A collection of circles. names : list of str, optional Individual names for the circles. colors : color or list of color, optional A color specification for the circles as a single color or a list of individual colors. layer : str, optional A layer path. clear : bool, optional Clear the layer before drawing. add_to_group : bool, optional Add the circles to a group. group_name : str, optional Name of the group. Returns ------- list The GUIDs of the created Rhino objects. """ circles = [] for circle in collection: circles.append({ 'plane': [list(circle[0][0]), list(circle[0][1])], 'radius': circle[1] }) if colors: if isinstance(colors[0], (int, float)): colors = iterable_like(collection, [colors], colors) else: colors = iterable_like(collection, colors, colors[0]) for point, rgb in zip(circles, colors): circle['color'] = rgb if names: if isinstance(names, basestring): names = iterable_like(collection, [names], names) else: names = iterable_like(collection, names, names[0]) for circle, name in zip(circles, names): circle['name'] = name guids = compas_rhino.draw_circles(circles, layer=layer, clear=clear) if not add_to_group: return guids group = compas_rhino.rs.AddGroup(group_name) if group: compas_rhino.rs.AddObjectsToGroup(guids, group) return group
def draw_collection(collection, names=None, colors=None, layer=None, clear=False, add_to_group=False, group_name=None): """Draw a collection of lines. Parameters ---------- collection: list of compas.geometry.Line A collection of ``Line`` objects. names : list of str, optional Individual names for the lines. colors : color or list of color, optional A color specification for the lines as a single color or a list of individual colors. layer : str, optional A layer path. clear : bool, optional Clear the layer before drawing. add_to_group : bool, optional Add the frames to a group. group_name : str, optional Name of the group. Returns ------- guids: list A list of GUIDs if the collection is not grouped. groupname: str The name of the group if the collection objects are grouped. """ lines = [{ 'start': list(line[0]), 'end': list(line[1]) } for line in collection] if colors: if isinstance(colors[0], (int, float)): colors = iterable_like(collection, [colors], colors) else: colors = iterable_like(collection, colors, colors[0]) for line, rgb in zip(lines, colors): line['color'] = rgb if names: if isinstance(names, basestring): names = iterable_like(collection, [names], names) else: names = iterable_like(collection, names, names[0]) for line, name in zip(lines, names): line['name'] = name guids = compas_rhino.draw_lines(lines, layer=layer, clear=clear) if not add_to_group: return guids group = compas_rhino.rs.AddGroup(group_name) if group: compas_rhino.rs.AddObjectsToGroup(guids, group) return group
def offset_polyline(polyline, distance, normal=[0.0, 0.0, 1.0], tol=1e-6): """Offset a polyline by a distance. Parameters ---------- polyline : list of point The XYZ coordinates of the vertices of a polyline. distance : float or list of tuples of floats The offset distance as float. A single value determines a constant offset globally. Alternatively, pairs of local offset values per line segment can be used to create variable offsets. Distance > 0: offset to the "left", distance < 0: offset to the "right". normal : vector The normal of the offset plane. Returns ------- offset polyline : list of point The XYZ coordinates of the resulting polyline. """ if not is_item_iterable(distance): distance = [distance] distances = iterable_like(polyline, distance, distance[-1]) segments = offset_segments(polyline, distances, normal) offset = [segments[0][0]] for s1, s2 in pairwise(segments): point = intersect(s1, s2, tol) offset.append(point) offset.append(segments[-1][1]) return offset
def size(self, size): size = size or self.default_size try: len(size) except TypeError: size = [size] size = iterable_like(self.points, size, self.default_size) self._size = list(size)
def color(self, color): color = color or self.default_color if not is_sequence_of_iterable(color): color = [color] self._color = [ FromArgb(*c) for c in iterable_like(self.lines, color, self.default_color) ]
def color(self, color): color = color or self.default_color if not is_sequence_of_iterable(color): color = [color] color = [ FromArgb(*color_to_rgb(c)) for c in iterable_like(self.points, color, self.default_color) ] self._color = color
def thickness(self, thickness): thickness = thickness or self.default_thickness try: len(thickness) except TypeError: thickness = [thickness] thickness = iterable_like(self.lines, thickness, self.default_thickness) self._thickness = list(thickness)
def color(self, color): if not color: return if not is_sequence_of_iterable(color): color = [color] color = [ FromArgb(*c) for c in iterable_like(self.faces, color, self.default_color) ] self._color = color
def offset_line(line, distance, normal=[0.0, 0.0, 1.0]): """Offset a line by a distance. Parameters ---------- line : tuple Two points defining the line. distances : float or list of floats The offset distance as float. A single value determines a constant offset. Alternatively, two offset values for the start and end point of the line can be used to a create variable offset. normal : vector The normal of the offset plane. Returns ------- offset line : tuple Two points defining the offset line. Notes ----- The offset direction is chosen such that if the line were along the positve X axis and the normal of the offset plane is along the positive Z axis, the offset line is in the direction of the postive Y axis. Examples -------- .. code-block:: python line = [(0.0, 0.0, 0.0), (3.0, 3.0, 0.0)] distance = 0.2 # constant offset line_offset = offset_line(line, distance) print(line_offset) distance = [0.2, 0.1] # variable offset line_offset = offset_line(line, distance) print(line_offset) """ a, b = line ab = subtract_vectors(b, a) direction = normalize_vector(cross_vectors(normal, ab)) if not is_item_iterable(distance): distance = [distance] distances = list(iterable_like(line, distance, distance[-1])) u = scale_vector(direction, distances[0]) v = scale_vector(direction, distances[1]) c = add_vectors(a, u) d = add_vectors(b, v) return c, d
def color(self, color): if not color: return if not is_sequence_of_iterable(color[0]): # the first item in the list should be a tuple of colors # if not, wrap the tuple color = [color] color = [(FromArgb(*bg), FromArgb(*text)) for bg, text in iterable_like(self.labels, color, ( self.default_color, self.default_textcolor))] self._color = color
def offset_polygon(polygon, distance, tol=1e-6): """Offset a polygon (closed) by a distance. Parameters ---------- polygon : sequence[point] | :class:`compas.geometry.Polygon` The XYZ coordinates of the corners of the polygon. The first and last coordinates must not be identical. distance : float | list[tuple[float, float]] The offset distance as float. A single value determines a constant offset globally. A list of pairs of local offset values per line segment can be used to create variable offsets. tol : float, optional A tolerance value for intersection calculations. Returns ------- list[[float, float, float]] The XYZ coordinates of the corners of the offset polygon. The first and last coordinates are identical. Notes ----- The offset direction is determined by the normal of the polygon. If the polygon is in the XY plane and the normal is along the positive Z axis, positive offset distances will result in an offset towards the inside of the polygon. The algorithm works also for spatial polygons that do not perfectly fit a plane. Examples -------- >>> """ normal = normal_polygon(polygon) if not is_item_iterable(distance): distance = [distance] distances = iterable_like(polygon, distance, distance[-1]) polygon = polygon + polygon[:1] segments = offset_segments(polygon, distances, normal) offset = [] for s1, s2 in pairwise(segments[-1:] + segments): point = intersect(s1, s2, tol) offset.append(point) return offset
def offset_polyline(polyline, distance, normal=[0.0, 0.0, 1.0], tol=1e-6): """Offset a polyline by a distance. Parameters ---------- polyline : sequence[point] | :class:`compas.geometry.Polyline` The XYZ coordinates of the vertices of a polyline. distance : float | list[tuple[float, float]] The offset distance as float. A single value determines a constant offset globally. Alternatively, pairs of local offset values per line segment can be used to create variable offsets. normal : [float, float, float] | :class:`compas.geometry.Vector`, optional The normal of the offset plane. tol : float, optional A tolerance value for intersection calculations. Returns ------- list[[float, float, float]] The XYZ coordinates of the resulting polyline. Notes ----- The offset direction is determined by the provided normal vector. If the polyline is in the XY plane and the normal is along the positive Z axis, positive offset distances will result in counterclockwise offsets, and negative values in clockwise direction. Examples -------- >>> """ if not is_item_iterable(distance): distance = [distance] distances = iterable_like(polyline, distance, distance[-1]) segments = offset_segments(polyline, distances, normal) offset = [segments[0][0]] for s1, s2 in pairwise(segments): point = intersect(s1, s2, tol) offset.append(point) offset.append(segments[-1][1]) return offset
def draw_collection(collection, color=None, layer=None, clear=False, group_collection=False, group_name=None): """Draw a collection of points. Parameters ---------- collection: list of compas.geometry.Point A collection of ``Point`` objects. color: tuple or list of tuple (optional) Color specification of the points. If one RGB color is provided, it will be applied to all points. If a list of RGB colors is provided, these colors are applied to the corresponding points. A list of colors should have the same length as the collection, with one color per item. Default value is ``None`` in which case the default point color of the artist is used. layer: str (optional) The layer in which the objects of the collection should be created. Default is ``None``, in which case the default layer setting of the artist is used. clear: bool (optional) Clear the layer before drawing. Default is ``False``. group_collection: bool (optional) Flag for grouping the objects of the collection. Default is ``False``. group_name: str (optional). The name of the group. Default is ``None``. Returns ------- guids: list A list of GUIDs if the collection is not grouped. groupname: str The name of the group if the collection objects are grouped. """ points = [] colors = iterable_like(collection, color) for point, rgb in zip(collection, colors): points.append({'pos': list(point), 'color': rgb}) guids = compas_rhino.draw_points(points, layer=layer, clear=clear) if not group_collection: return guids group = compas_rhino.rs.AddGroup(group_name) if group: compas_rhino.rs.AddObjectsToGroup(guids, group) return group
def offset_line(line, distance, normal=[0.0, 0.0, 1.0]): """Offset a line by a distance. Parameters ---------- line : [point, point] | :class:`compas.geometry.Line` A line defined by two points. distances : float or list[float] The offset distance as float. A single value determines a constant offset. A list of two offset values can be used to a create variable offset at the start and end. normal : [float, float, float] | :class:`compas.geometry.Vector`, optional The normal of the offset plane. Returns ------- tuple[[float, float, float], [float, float, float]] Two points defining the offset line. Notes ----- The offset direction is chosen such that if the line were along the positve X axis and the normal of the offset plane is along the positive Z axis, the offset line is in the direction of the postive Y axis. Examples -------- >>> """ a, b = line ab = subtract_vectors(b, a) direction = normalize_vector(cross_vectors(normal, ab)) if not is_item_iterable(distance): distance = [distance] distances = list(iterable_like(line, distance, distance[-1])) u = scale_vector(direction, distances[0]) v = scale_vector(direction, distances[1]) c = add_vectors(a, u) d = add_vectors(b, v) return c, d
def test_iterable_like_string_and_float(target, base, fillvalue): a = list(iterable_like(target, base, fillvalue)) assert a == [0.5, 0.5, 0.5, 0.5, 0.5]
def offset_polygon(polygon, distance, tol=1e-6): """Offset a polygon (closed) by a distance. Parameters ---------- polygon : list of point The XYZ coordinates of the corners of the polygon. The first and last coordinates must not be identical. distance : float or list of float The offset distance as float. A single value determines a constant offset globally. Alternatively, pairs of local offset values per line segment can be used to create variable offsets. Distance > 0: offset to the outside, distance < 0: offset to the inside. Returns ------- offset polygon : list of point The XYZ coordinates of the corners of the offset polygon. The first and last coordinates are identical. Notes ----- The offset direction is determined by the normal of the polygon. If the polygon is in the XY plane and the normal is along the positive Z axis, positive offset distances will result in an offset towards the inside of the polygon. The algorithm works also for spatial polygons that do not perfectly fit a plane. Examples -------- .. code-block:: python polygon = [ (0.0, 0.0, 0.0), (3.0, 0.0, 1.0), (3.0, 3.0, 2.0), (1.5, 1.5, 2.0), (0.0, 3.0, 1.0), (0.0, 0.0, 0.0) ] distance = 0.5 # constant offset polygon_offset = offset_polygon(polygon, distance) print(polygon_offset) distance = [ (0.1, 0.2), (0.2, 0.3), (0.3, 0.4), (0.4, 0.3), (0.3, 0.1) ] # variable offset polygon_offset = offset_polygon(polygon, distance) print(polygon_offset) """ normal = normal_polygon(polygon) if not is_item_iterable(distance): distance = [distance] distances = iterable_like(polygon, distance, distance[-1]) polygon = polygon + polygon[:1] segments = offset_segments(polygon, distances, normal) offset = [] for s1, s2 in pairwise(segments[-1:] + segments): point = intersect(s1, s2, tol) offset.append(point) return offset
def test_iterable_cap_generator(mesh_a, mesh_b): ma = Mesh.from_obj(compas.get(mesh_a)) mb = Mesh.from_obj(compas.get(mesh_b)) a = list(iterable_like(ma.faces(), mb.faces())) assert len(a) == len(list(ma.faces()))
def test_iterable_like_generator_and_list(target, base, fillvalue): a = list(iterable_like(target, base, fillvalue)) assert a == ['a', 'b']
def draw_collection(collection, color=None, name=None, layer=None, clear=False, group_collection=False, group_name=None): """Draw a collection of points. Parameters ---------- collection : list of compas.geometry.Point A collection of ``Point`` objects. color : tuple or list of tuple (optional) Color specification of the points. If one RGB color is provided, it will be applied to all points. If a list of RGB colors is provided, these colors are applied to the corresponding points. A list of colors should have the same length as the collection, with one color per item. Default value is ``None`` in which case the default point color of the artist is used. name : str or list of str (optional) Name of each point. If only one name is provided, this name will be used for all points. If a list of names is provided, the list should contain a name for each point in the collection. layer : str (optional) The layer in which the objects of the collection should be created. Default is ``None``, in which case the default layer setting of the artist is used. clear : bool (optional) Clear the layer before drawing. Default is ``False``. group_collection : bool (optional) Flag for grouping the objects of the collection. Default is ``False``. group_name : str (optional). The name of the group. Default is ``None``. Returns ------- guids: list A list of GUIDs if the collection is not grouped. groupname: str The name of the group if the collection objects are grouped. """ compas_rhino.rs.EnableRedraw(False) points = [{'pos': list(point)} for point in collection] if color: if isinstance(color[0], (int, float)): colors = iterable_like(collection, [color], color) else: colors = iterable_like(collection, color, color[0]) for point, rgb in zip(points, colors): point['color'] = rgb if name: if isinstance(name, basestring): names = iterable_like(collection, [name], name) else: names = iterable_like(collection, name, name[0]) for point, name in zip(points, names): point['name'] = name guids = compas_rhino.draw_points(points, layer=layer, clear=clear, redraw=False) if not group_collection: return guids group = compas_rhino.rs.AddGroup(group_name) if group: compas_rhino.rs.AddObjectsToGroup(guids, group) compas_rhino.rs.EnableRedraw(True) return group
def mesh_subdivide_frames(mesh, offset, add_windows=False): """Subdivide a mesh by creating offset frames and windows on its faces. Parameters ---------- mesh : :class:`compas.datastructures.Mesh` The mesh object to be subdivided. offset : float | dict[int, float] The offset distance to create the frames. A single value will result in a constant offset everywhere. A dictionary mapping faces to offset values will be processed accordingly. add_windows : bool, optional If True, add a window face in the frame opening. Returns ------- :class:`compas.datastructures.Mesh` A new subdivided mesh. """ cls = type(mesh) SubdMesh = subd_factory(cls) subd = SubdMesh() # 0. pre-compute offset distances if not isinstance(offset, dict): distances = iterable_like(mesh.faces(), [offset], offset) offset = {fkey: od for fkey, od in zip(mesh.faces(), distances)} # 1. add vertices newkeys = {} for vkey, attr in mesh.vertices(True): newkeys[vkey] = subd.add_vertex(*mesh.vertex_coordinates(vkey)) # 2. add faces for fkey in mesh.faces(): face = [newkeys[vkey] for vkey in mesh.face_vertices(fkey)] d = offset.get(fkey) # 2a. add face and break if no offset is found if d is None: subd.add_face(face) continue polygon = offset_polygon(mesh.face_coordinates(fkey), d) # 2a. add offset vertices window = [] for xyz in polygon: x, y, z = xyz new_vkey = subd.add_vertex(x=x, y=y, z=z) window.append(new_vkey) # 2b. frame faces face = face + face[:1] window = window + window[:1] for sa, sb in zip(pairwise(face), pairwise(window)): subd.add_face([sa[0], sa[1], sb[1], sb[0]]) # 2c. window face if add_windows: subd.add_face(window) return cls.from_data(subd.data)
def mesh_subdivide_frames(mesh, offset, add_windows=False): """Subdivide a mesh by creating offset frames and windows on its faces. Parameters ---------- mesh : Mesh The mesh object to be subdivided. offset : float or dict The offset distance to create the frames. A single value will result in a constant offset everywhere. A dictionary mapping facekey: offset will be processed accordingly. add_windows : boolean Optional. Flag to add window face. Default is ``False``. Returns ------- Mesh A new subdivided mesh. Examples -------- >>> """ subd = SubdMesh() # 0. pre-compute offset distances if not isinstance(offset, dict): distances = iterable_like(mesh.faces(), [offset], offset) offset = {fkey: od for fkey, od in zip(mesh.faces(), distances)} # 1. add vertices newkeys = {} for vkey, attr in mesh.vertices(True): newkeys[vkey] = subd.add_vertex(*mesh.vertex_coordinates(vkey)) # 2. add faces for fkey in mesh.faces(): face = [newkeys[vkey] for vkey in mesh.face_vertices(fkey)] d = offset.get(fkey) # 2a. add face and break if no offset is found if d is None: subd.add_face(face) continue polygon = offset_polygon(mesh.face_coordinates(fkey), d) # 2a. add offset vertices window = [] for xyz in polygon: x, y, z = xyz new_vkey = subd.add_vertex(x=x, y=y, z=z) window.append(new_vkey) # 2b. frame faces face = face + face[:1] window = window + window[:1] for sa, sb in zip(pairwise(face), pairwise(window)): subd.add_face([sa[0], sa[1], sb[1], sb[0]]) # 2c. window face if add_windows: subd.add_face(window) return subd
def test_iterable_like_list_and_dict(target, base, fillvalue): a = list(iterable_like(target, base, fillvalue)) assert a == ['key_1', 'key_2', 'key_3']