diff --git a/CHANGELOG.md b/CHANGELOG.md index ca43c5824747..43d911dda212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `__add__`, `__sub__`, `__and__` to `compas.geometry.Shape` for boolean operations using binary operators. * Added `is_closed` to `compas.geometry.Polyhedron`. * Added `Plane.offset`. +* Added `compas.artists.Artist`. +* Added pluggable `compas.artists.new_artist`. +* Added plugin `compas_rhino.artists.new_artist_rhino`. +* Added plugin `compas_blender.artists.new_artist_blender`. +* Added `compas.artist.DataArtistNotRegistered`. * Added `draw_node_labels` and `draw_edgelabels` to `compas_blender.artists.NetworkArtist`. * Added `compas_blender.artists.RobotModelArtist.clear`. * Added `compas_blender.geometry.booleans` as plugin for boolean pluggables. @@ -23,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * Fixed bug in `compas_blender.draw_texts`. +* Changed `compas_rhino.artists.BaseArtist` to `compas_rhino.artists.RhinoArtist`. +* Changed `compas_blender.artists.BaseArtist` to `compas_blender.artists.BlenderArtist`. * Changed default resolution for shape discretisation to 16 for both u and v where relevant. * Changed base class of `compas.geometry.Primitive` and `compas.geometry.Shape` to `compas.geometry.Geometry`. * `compas_blender.artists.RobotModelArtist.collection` can be assigned as a Blender collection or a name. diff --git a/docs/_images/tutorial/plotters_dynamic.gif b/docs/_images/tutorial/plotters_dynamic.gif index edff60f5cd96..f04f5d41c674 100644 Binary files a/docs/_images/tutorial/plotters_dynamic.gif and b/docs/_images/tutorial/plotters_dynamic.gif differ diff --git a/docs/_images/tutorial/plotters_line-options.png b/docs/_images/tutorial/plotters_line-options.png index a64f76f5b028..57df6fad80bd 100644 Binary files a/docs/_images/tutorial/plotters_line-options.png and b/docs/_images/tutorial/plotters_line-options.png differ diff --git a/docs/_images/tutorial/plotters_vector-options.png b/docs/_images/tutorial/plotters_vector-options.png index f0e719f64d53..d24b97034c39 100644 Binary files a/docs/_images/tutorial/plotters_vector-options.png and b/docs/_images/tutorial/plotters_vector-options.png differ diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst deleted file mode 100644 index b7556ebf7b06..000000000000 --- a/docs/_templates/autosummary/base.rst +++ /dev/null @@ -1,5 +0,0 @@ -{{ fullname | escape | underline}} - -.. currentmodule:: {{ module }} - -.. auto{{ objtype }}:: {{ objname }} diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst deleted file mode 100644 index bb0043529c29..000000000000 --- a/docs/_templates/autosummary/class.rst +++ /dev/null @@ -1,28 +0,0 @@ -{{ fullname | escape | underline}} - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - - {% block methods %} - - {% if methods %} - .. rubric:: {{ _('Methods') }} - - .. autosummary:: - {% for item in methods %} - ~{{ name }}.{{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block attributes %} - {% if attributes %} - .. rubric:: {{ _('Attributes') }} - - .. autosummary:: - {% for item in attributes %} - ~{{ name }}.{{ item }} - {%- endfor %} - {% endif %} - {% endblock %} diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst deleted file mode 100644 index df86398b25ff..000000000000 --- a/docs/_templates/autosummary/module.rst +++ /dev/null @@ -1,3 +0,0 @@ -{{ fullname | escape | underline}} - -.. automodule:: {{ fullname }} diff --git a/docs/api/compas.artists.rst b/docs/api/compas.artists.rst new file mode 100644 index 000000000000..fcf5a7d533bf --- /dev/null +++ b/docs/api/compas.artists.rst @@ -0,0 +1,2 @@ + +.. automodule:: compas.artists diff --git a/docs/conf.py b/docs/conf.py index c7ee64ea55b9..5706a1f39b38 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -350,6 +350,6 @@ def linkcode_resolve(domain, info): html_copy_source = False html_show_sourcelink = False html_permalinks = False -html_add_permalinks = "" +html_permalinks_icon = "" html_experimental_html5_writer = True html_compact_lists = True diff --git a/docs/devguide.rst b/docs/devguide.rst index 23a4b77dd4c7..2ecdc1aa6cda 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -390,10 +390,12 @@ Advanced options There are a few additional options that plugins can use: -* ``requires``: List of required python modules. COMPAS will filter out plugins if their +* ``requires``: List of requirements. COMPAS will filter out plugins if their requirements list is not satisfied at runtime. This allows to have multiple implementations - of the same operation and have them selected based on which packages are installed. - on the system. Eg. `requires=['scipy']`. + of the same operation and have them selected based on different criteria. + The requirement can either be a package name string (e.g. ``requires=['scipy']``) or + a ``callable`` with a boolean return value, in which any arbitrary check can be implemented + (e.g. ``requires=[lambda: is_rhino_active()]``). * ``tryfirst`` and ``trylast``: Plugins cannot control the exact priority they will have but they can indicate whether to try to prioritize them or demote them as fallback using these two boolean parameters. diff --git a/docs/tutorial/plotters_line-options.py b/docs/tutorial/plotters_line-options.py index 51ce46ee405d..d2f46e34e668 100644 --- a/docs/tutorial/plotters_line-options.py +++ b/docs/tutorial/plotters_line-options.py @@ -18,4 +18,5 @@ draw_points=True) plotter.zoom_extents() -plotter.save('docs/_images/tutorial/plotters_line-options.png', dpi=300) +plotter.show() +# plotter.save('docs/_images/tutorial/plotters_line-options.png', dpi=300) diff --git a/docs/tutorial/plotters_point-options.py b/docs/tutorial/plotters_point-options.py index 06b275ecfd24..c1cb9a28a1d0 100644 --- a/docs/tutorial/plotters_point-options.py +++ b/docs/tutorial/plotters_point-options.py @@ -10,4 +10,5 @@ for point in pointcloud: plotter.add(point, size=random.randint(1, 10), edgecolor=i_to_rgb(random.random(), normalize=True)) plotter.zoom_extents() -plotter.save('docs/_images/tutorial/plotters_point-options.png', dpi=300) +plotter.show() +# plotter.save('docs/_images/tutorial/plotters_point-options.png', dpi=300) diff --git a/docs/tutorial/plotters_polygon-options.py b/docs/tutorial/plotters_polygon-options.py index a6da391c9281..e13441ccde12 100644 --- a/docs/tutorial/plotters_polygon-options.py +++ b/docs/tutorial/plotters_polygon-options.py @@ -11,4 +11,5 @@ plotter.add(poly2, linestyle='dashed', facecolor=(1.0, 0.8, 0.8), edgecolor=(1.0, 0.0, 0.0)) plotter.add(poly3, alpha=0.5) plotter.zoom_extents() -plotter.save('docs/_images/tutorial/plotters_polygon-options.png', dpi=300) +plotter.show() +# plotter.save('docs/_images/tutorial/plotters_polygon-options.png', dpi=300) diff --git a/docs/tutorial/plotters_vector-options.py b/docs/tutorial/plotters_vector-options.py index d3d84d86d8f8..63c2cf9b5280 100644 --- a/docs/tutorial/plotters_vector-options.py +++ b/docs/tutorial/plotters_vector-options.py @@ -15,4 +15,5 @@ plotter.add(b, size=10, edgecolor=(1, 0, 0)) plotter.zoom_extents() +plotter.show() plotter.save('docs/_images/tutorial/plotters_vector-options.png', dpi=300) diff --git a/src/compas/__init__.py b/src/compas/__init__.py index a27da05e67bf..8ecc1b5f9bca 100644 --- a/src/compas/__init__.py +++ b/src/compas/__init__.py @@ -9,6 +9,7 @@ :maxdepth: 1 :titlesonly: + compas.artists compas.data compas.datastructures compas.files diff --git a/src/compas/artists/__init__.py b/src/compas/artists/__init__.py new file mode 100644 index 000000000000..20b6470ca1ee --- /dev/null +++ b/src/compas/artists/__init__.py @@ -0,0 +1,58 @@ +""" +******************************************************************************** +artists +******************************************************************************** + +.. currentmodule:: compas.artists + +Classes +======= + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + Artist + RobotModelArtist + MeshArtist + NetworkArtist + PrimitiveArtist + ShapeArtist + VolMeshArtist + + +Exceptions +========== + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + DataArtistNotRegistered + +""" +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from .exceptions import DataArtistNotRegistered +from .artist import Artist +from .meshartist import MeshArtist +from .networkartist import NetworkArtist +from .primitiveartist import PrimitiveArtist +from .robotmodelartist import RobotModelArtist +from .shapeartist import ShapeArtist +from .volmeshartist import VolMeshArtist + +BaseRobotModelArtist = RobotModelArtist + +__all__ = [ + 'DataArtistNotRegistered', + 'Artist', + 'MeshArtist', + 'NetworkArtist', + 'PrimitiveArtist', + 'RobotModelArtist', + 'ShapeArtist', + 'VolMeshArtist', +] diff --git a/src/compas/artists/artist.py b/src/compas/artists/artist.py new file mode 100644 index 000000000000..4eab45234006 --- /dev/null +++ b/src/compas/artists/artist.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from abc import abstractmethod +from compas.plugins import pluggable + + +@pluggable(category='drawing-utils') +def clear(): + raise NotImplementedError + + +@pluggable(category='drawing-utils') +def redraw(): + raise NotImplementedError + + +@pluggable(category='factories') +def new_artist(cls, *args, **kwargs): + raise NotImplementedError + + +class Artist(object): + """Base class for all artists. + """ + + ITEM_ARTIST = {} + + def __new__(cls, *args, **kwargs): + return new_artist(cls, *args, **kwargs) + + @staticmethod + def clear(): + return clear() + + @staticmethod + def redraw(): + return redraw() + + @staticmethod + def register(item_type, artist_type): + Artist.ITEM_ARTIST[item_type] = artist_type + + @abstractmethod + def draw(self): + raise NotImplementedError + + @staticmethod + def draw_collection(collection): + raise NotImplementedError diff --git a/src/compas/artists/assemblyartist.py b/src/compas/artists/assemblyartist.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/compas/robots/base_artist/__init__.py b/src/compas/artists/exceptions.py similarity index 51% rename from src/compas/robots/base_artist/__init__.py rename to src/compas/artists/exceptions.py index a1d5b96942d3..7c9a92a8d5ff 100644 --- a/src/compas/robots/base_artist/__init__.py +++ b/src/compas/artists/exceptions.py @@ -1,7 +1,7 @@ +from __future__ import print_function from __future__ import absolute_import from __future__ import division -from __future__ import print_function -from ._artist import * # noqa: F401 F403 -__all__ = [name for name in dir() if not name.startswith('_')] +class DataArtistNotRegistered(Exception): + pass diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py new file mode 100644 index 000000000000..515b0002c74a --- /dev/null +++ b/src/compas/artists/meshartist.py @@ -0,0 +1,344 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from abc import abstractmethod + +from compas.utilities import is_color_rgb +from .artist import Artist + + +class MeshArtist(Artist): + """Base class for all mesh artists. + + Parameters + ---------- + mesh : :class:`compas.datastructures.Mesh` + A COMPAS mesh. + + Class Attributes + ---------------- + default_color : tuple + The default color of the mesh. + default_vertexcolor : tuple + The default color for vertices that do not have a specified color. + default_edgecolor : tuple + The default color for edges that do not have a specified color. + default_facecolor : tuple + The default color for faces that do not have a specified color. + default_vertexsize : int + default_edgewidth : float + + Attributes + ---------- + mesh : :class:`compas.datastructures.Mesh` + The mesh associated with the artist. + vertices : list + The vertices to include in the drawing. + Default is all vertices. + edges : list + The edges to include in the drawing. + Default is all edges. + faces : list + The faces to include in the drawing. + Default is all faces. + vertex_xyz : dict + The view coordinates of the vertices. + Default is to use the actual vertex coordinates. + vertex_color : dict + Mapping between vertices and colors. + Default is to use the default color for vertices. + edge_color : dict + Mapping between edges and colors. + Default is to use the default color for edges. + face_color : dict + Mapping between faces and colors. + Default is to use the default color for faces. + vertex_text : dict + Mapping between vertices and text labels. + edge_text : dict + Mapping between edges and text labels. + face_text : dict + Mapping between faces and text labels. + """ + + default_color = (0.0, 0.0, 0.0) + default_vertexcolor = (1.0, 1.0, 1.0) + default_edgecolor = (0.0, 0.0, 0.0) + default_facecolor = (1.0, 1.0, 1.0) + + default_vertexsize = 5 + default_edgewidth = 1.0 + + def __init__(self, mesh, **kwargs): + super(MeshArtist, self).__init__(**kwargs) + + self._mesh = None + self._vertices = None + self._edges = None + self._faces = None + self._color = None + self._vertex_xyz = None + self._vertex_color = None + self._vertex_text = None + self._vertex_size = None + self._edge_color = None + self._edge_text = None + self._edge_width = None + self._face_color = None + self._face_text = None + + self._vertexcollection = None + self._edgecollection = None + self._facecollection = None + self._vertexnormalcollection = None + self._facenormalcollection = None + self._vertexlabelcollection = None + self._edgelabelcollection = None + self._facelabelcollection = None + + self.mesh = mesh + + @property + def mesh(self): + return self._mesh + + @mesh.setter + def mesh(self, mesh): + self._mesh = mesh + self._vertex_xyz = None + + @property + def vertices(self): + if self._vertices is None: + self._vertices = list(self.mesh.vertices()) + return self._vertices + + @vertices.setter + def vertices(self, vertices): + self._vertices = vertices + + @property + def edges(self): + if self._edges is None: + self._edges = list(self.mesh.edges()) + return self._edges + + @edges.setter + def edges(self, edges): + self._edges = edges + + @property + def faces(self): + if self._faces is None: + self._faces = list(self.mesh.faces()) + return self._faces + + @faces.setter + def faces(self, faces): + self._faces = faces + + @property + def color(self): + if not self._color: + self._color = self.default_color + return self._color + + @color.setter + def color(self, color): + if is_color_rgb(color): + self._color = color + + @property + def vertex_xyz(self): + if self._vertex_xyz is None: + return {vertex: self.mesh.vertex_attributes(vertex, 'xyz') for vertex in self.mesh.vertices()} + return self._vertex_xyz + + @vertex_xyz.setter + def vertex_xyz(self, vertex_xyz): + self._vertex_xyz = vertex_xyz + + @property + def vertex_color(self): + if self._vertex_color is None: + self._vertex_color = {vertex: self.default_vertexcolor for vertex in self.mesh.vertices()} + return self._vertex_color + + @vertex_color.setter + def vertex_color(self, vertex_color): + if isinstance(vertex_color, dict): + self._vertex_color = vertex_color + elif is_color_rgb(vertex_color): + self._vertex_color = {vertex: vertex_color for vertex in self.mesh.vertices()} + + @property + def vertex_text(self): + if self._vertex_text is None: + self._vertex_text = {vertex: str(vertex) for vertex in self.mesh.vertices()} + return self._vertex_text + + @vertex_text.setter + def vertex_text(self, text): + if text == 'key': + self._vertex_text = {vertex: str(vertex) for vertex in self.mesh.vertices()} + elif text == 'index': + self._vertex_text = {vertex: str(index) for index, vertex in enumerate(self.mesh.vertices())} + elif isinstance(text, dict): + self._vertex_text = text + + @property + def vertex_size(self): + if not self._vertex_size: + self._vertex_size = {vertex: self.default_vertexsize for vertex in self.mesh.vertices()} + return self._vertex_size + + @vertex_size.setter + def vertex_size(self, vertexsize): + if isinstance(vertexsize, dict): + self._vertex_size = vertexsize + elif isinstance(vertexsize, (int, float)): + self._vertex_size = {vertex: vertexsize for vertex in self.mesh.vertices()} + + @property + def edge_color(self): + if self._edge_color is None: + self._edge_color = {edge: self.default_edgecolor for edge in self.mesh.edges()} + return self._edge_color + + @edge_color.setter + def edge_color(self, edge_color): + if isinstance(edge_color, dict): + self._edge_color = edge_color + elif is_color_rgb(edge_color): + self._edge_color = {edge: edge_color for edge in self.mesh.edges()} + + @property + def edge_text(self): + if self._edge_text is None: + self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.mesh.edges()} + return self._edge_text + + @edge_text.setter + def edge_text(self, text): + if text == 'key': + self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.mesh.edges()} + elif text == 'index': + self._edge_text = {edge: str(index) for index, edge in enumerate(self.mesh.edges())} + elif isinstance(text, dict): + self._edge_text = text + + @property + def edge_width(self): + if not self._edge_width: + self._edge_width = {edge: self.default_edgewidth for edge in self.mesh.edges()} + return self._edge_width + + @edge_width.setter + def edge_width(self, edgewidth): + if isinstance(edgewidth, dict): + self._edge_width = edgewidth + elif isinstance(edgewidth, (int, float)): + self._edge_width = {edge: edgewidth for edge in self.mesh.edges()} + + @property + def face_color(self): + if self._face_color is None: + self._face_color = {face: self.default_facecolor for face in self.mesh.faces()} + return self._face_color + + @face_color.setter + def face_color(self, face_color): + if isinstance(face_color, dict): + self._face_color = face_color + elif is_color_rgb(face_color): + self._face_color = {face: face_color for face in self.mesh.faces()} + + @property + def face_text(self): + if self._face_text is None: + self._face_text = {face: str(face) for face in self.mesh.faces()} + return self._face_text + + @face_text.setter + def face_text(self, text): + if text == 'key': + self._face_text = {face: str(face) for face in self.mesh.faces()} + elif text == 'index': + self._face_text = {face: str(index) for index, face in enumerate(self.mesh.faces())} + elif isinstance(text, dict): + self._face_text = text + + @abstractmethod + def draw_vertices(self, vertices=None, color=None, text=None): + """Draw the vertices of the mesh. + + Parameters + ---------- + vertices : list, optional + The vertices to include in the drawing. + Default is all vertices. + color : tuple or dict, optional + The color of the vertices, + as either a single color to be applied to all vertices, + or a color dict, mapping specific vertices to specific colors. + text : dict, optional + The text labels for the vertices + as a text dict, mapping specific vertices to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def draw_edges(self, edges=None, color=None, text=None): + """Draw the edges of the mesh. + + Parameters + ---------- + edges : list, optional + The edges to include in the drawing. + Default is all edges. + color : tuple or dict, optional + The color of the edges, + as either a single color to be applied to all edges, + or a color dict, mapping specific edges to specific colors. + text : dict, optional + The text labels for the edges + as a text dict, mapping specific edges to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def draw_faces(self, faces=None, color=None, text=None): + """Draw the faces of the mesh. + + Parameters + ---------- + faces : list, optional + The faces to include in the drawing. + Default is all faces. + color : tuple or dict, optional + The color of the faces, + as either a single color to be applied to all faces, + or a color dict, mapping specific faces to specific colors. + text : dict, optional + The text labels for the faces + as a text dict, mapping specific faces to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def clear_vertices(self): + raise NotImplementedError + + @abstractmethod + def clear_edges(self): + raise NotImplementedError + + @abstractmethod + def clear_faces(self): + raise NotImplementedError + + def clear(self): + self.clear_vertices() + self.clear_edges() + self.clear_faces() diff --git a/src/compas/artists/networkartist.py b/src/compas/artists/networkartist.py new file mode 100644 index 000000000000..3ed343e9d055 --- /dev/null +++ b/src/compas/artists/networkartist.py @@ -0,0 +1,250 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from abc import abstractmethod + +from compas.utilities import is_color_rgb +from .artist import Artist + + +class NetworkArtist(Artist): + """Artist for drawing network data structures. + + Parameters + ---------- + network : :class:`compas.datastructures.Network` + A COMPAS network. + + Class Attributes + ---------------- + default_nodecolor : tuple + The default color for nodes that do not have a specified color. + default_edgecolor : tuple + The default color for edges that do not have a specified color. + + Attributes + ---------- + network : :class:`compas.datastructures.Network` + The COMPAS network associated with the artist. + nodes : list + The list of nodes to draw. + Default is a list of all nodes of the network. + edges : list + The list of edges to draw. + Default is a list of all edges of the network. + node_xyz : dict[int, tuple(float, float, float)] + Mapping between nodes and their view coordinates. + The default view coordinates are the actual coordinates of the nodes of the network. + node_color : dict[int, tuple(int, int, int)] + Mapping between nodes and RGB color values. + The colors have to be integer tuples with values in the range ``0-255``. + Missing nodes get the default node color (``MeshArtist.default_nodecolor``). + node_text : dict[int, str] + Mapping between nodes and text labels. + Missing nodes are labelled with the corresponding node identifiers. + edge_color : dict[tuple(int, int), tuple(int, int, int)] + Mapping between edges and RGB color values. + The colors have to be integer tuples with values in the range ``0-255``. + Missing edges get the default edge color (``MeshArtist.default_edgecolor``). + edge_text : dict[tuple(int, int), str] + Mapping between edges and text labels. + Missing edges are labelled with the corresponding edge identifiers. + + """ + + default_nodecolor = (1, 1, 1) + default_edgecolor = (0, 0, 0) + + default_nodesize = 5 + default_edgewidth = 1.0 + + def __init__(self, network, **kwargs): + super(NetworkArtist, self).__init__(**kwargs) + + self._network = None + self._nodes = None + self._edges = None + self._node_xyz = None + self._node_color = None + self._edge_color = None + self._node_text = None + self._edge_text = None + + self._nodecollection = None + self._edgecollection = None + self._nodelabelcollection = None + self._edgelabelcollection = None + + self.network = network + + @property + def network(self): + return self._network + + @network.setter + def network(self, network): + self._network = network + self._node_xyz = None + + @property + def nodes(self): + if self._nodes is None: + self._nodes = list(self.network.nodes()) + return self._nodes + + @nodes.setter + def nodes(self, nodes): + self._nodes = nodes + + @property + def edges(self): + if self._edges is None: + self._edges = list(self.network.edges()) + return self._edges + + @edges.setter + def edges(self, edges): + self._edges = edges + + @property + def node_xyz(self): + if not self._node_xyz: + return {node: self.network.node_attributes(node, 'xyz') for node in self.network.nodes()} + return self._node_xyz + + @node_xyz.setter + def node_xyz(self, node_xyz): + self._node_xyz = node_xyz + + @property + def node_color(self): + if not self._node_color: + self._node_color = {node: self.default_nodecolor for node in self.network.nodes()} + return self._node_color + + @node_color.setter + def node_color(self, node_color): + if isinstance(node_color, dict): + self._node_color = node_color + elif is_color_rgb(node_color): + self._node_color = {node: node_color for node in self.network.nodes()} + + @property + def node_size(self): + if not self._node_size: + self._node_size = {node: self.default_nodesize for node in self.network.vertices()} + return self._node_size + + @node_size.setter + def node_size(self, nodesize): + if isinstance(nodesize, dict): + self._node_size = nodesize + elif isinstance(nodesize, (int, float)): + self._node_size = {node: nodesize for node in self.network.vertices()} + + @property + def edge_color(self): + if not self._edge_color: + self._edge_color = {edge: self.default_edgecolor for edge in self.network.edges()} + return self._edge_color + + @edge_color.setter + def edge_color(self, edge_color): + if isinstance(edge_color, dict): + self._edge_color = edge_color + elif is_color_rgb(edge_color): + self._edge_color = {edge: edge_color for edge in self.network.edges()} + + @property + def node_text(self): + if not self._node_text: + self._node_text = {node: str(node) for node in self.network.nodes()} + return self._node_text + + @node_text.setter + def node_text(self, text): + if text == 'key': + self._node_text = {node: str(node) for node in self.network.nodes()} + elif text == 'index': + self._node_text = {node: str(index) for index, node in enumerate(self.network.nodes())} + elif isinstance(text, dict): + self._node_text = text + + @property + def edge_text(self): + if not self._edge_text: + self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.network.edges()} + return self._edge_text + + @edge_text.setter + def edge_text(self, text): + if text == 'key': + self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.network.edges()} + elif text == 'index': + self._edge_text = {edge: str(index) for index, edge in enumerate(self.network.edges())} + elif isinstance(text, dict): + self._edge_text = text + + @property + def edge_width(self): + if not self._edge_width: + self._edge_width = {edge: self.default_edgewidth for edge in self.network.edges()} + return self._edge_width + + @edge_width.setter + def edge_width(self, edgewidth): + if isinstance(edgewidth, dict): + self._edge_width = edgewidth + elif isinstance(edgewidth, (int, float)): + self._edge_width = {edge: edgewidth for edge in self.network.edges()} + + @abstractmethod + def draw_nodes(self, nodes=None, color=None, text=None): + """Draw the nodes of the network. + + Parameters + ---------- + nodes : list, optional + The nodes to include in the drawing. + Default is all nodes. + color : tuple or dict, optional + The color of the nodes, + as either a single color to be applied to all nodes, + or a color dict, mapping specific nodes to specific colors. + text : dict, optional + The text labels for the nodes + as a text dict, mapping specific nodes to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def draw_edges(self, edges=None, color=None, text=None): + """Draw the edges of the network. + + Parameters + ---------- + edges : list, optional + The edges to include in the drawing. + Default is all edges. + color : tuple or dict, optional + The color of the edges, + as either a single color to be applied to all edges, + or a color dict, mapping specific edges to specific colors. + text : dict, optional + The text labels for the edges + as a text dict, mapping specific edges to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def clear_nodes(self): + raise NotImplementedError + + @abstractmethod + def clear_edges(self): + raise NotImplementedError + + def clear(self): + self.clear_nodes() + self.clear_edges() diff --git a/src/compas/artists/primitiveartist.py b/src/compas/artists/primitiveartist.py new file mode 100644 index 000000000000..a3d0328cc8d4 --- /dev/null +++ b/src/compas/artists/primitiveartist.py @@ -0,0 +1,59 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.utilities import is_color_rgb +from .artist import Artist + + +class PrimitiveArtist(Artist): + """Base class for artists for geometry primitives. + + Parameters + ---------- + primitive: :class:`compas.geometry.Primitive` + The geometry of the primitive. + color : tuple, optional + The RGB components of the base color of the primitive. + + Class Attributes + ---------------- + default_color : tuple + The default rgb color value of the primitive (``(0, 0, 0)``). + + Attributes + ---------- + primitive: :class:`compas.geometry.Primitive` + The geometry of the primitive. + color : tuple + The RGB components of the base color of the primitive. + + """ + + default_color = (0, 0, 0) + + def __init__(self, primitive, color=None, **kwargs): + super(PrimitiveArtist, self).__init__() + self._primitive = None + self._color = None + self.primitive = primitive + self.color = color + + @property + def primitive(self): + return self._primitive + + @primitive.setter + def primitive(self, primitive): + self._primitive = primitive + + @property + def color(self): + if not self._color: + self._color = self.default_color + return self._color + + @color.setter + def color(self, color): + if is_color_rgb(color): + self._color = color diff --git a/src/compas/robots/base_artist/_artist.py b/src/compas/artists/robotmodelartist.py similarity index 99% rename from src/compas/robots/base_artist/_artist.py rename to src/compas/artists/robotmodelartist.py index e0174e8fcfda..9dbf4997de4a 100644 --- a/src/compas/robots/base_artist/_artist.py +++ b/src/compas/artists/robotmodelartist.py @@ -10,13 +10,11 @@ from compas.robots import Geometry from compas.robots.model.link import LinkItem - -__all__ = [ - 'BaseRobotModelArtist' -] +from .artist import Artist class AbstractRobotModelArtist(object): + def transform(self, geometry, transformation): """Transforms a CAD-specific geometry using a **COMPAS** transformation. @@ -51,7 +49,7 @@ def create_geometry(self, geometry, name=None, color=None): raise NotImplementedError -class BaseRobotModelArtist(AbstractRobotModelArtist): +class RobotModelArtist(AbstractRobotModelArtist, Artist): """Provides common functionality to most robot model artist implementations. In **COMPAS**, the `artists` are classes that assist with the visualization of @@ -71,7 +69,7 @@ class BaseRobotModelArtist(AbstractRobotModelArtist): """ def __init__(self, model): - super(BaseRobotModelArtist, self).__init__() + super(RobotModelArtist, self).__init__() self.model = model self.create() self.scale_factor = 1. diff --git a/src/compas/artists/shapeartist.py b/src/compas/artists/shapeartist.py new file mode 100644 index 000000000000..097a05162dcd --- /dev/null +++ b/src/compas/artists/shapeartist.py @@ -0,0 +1,90 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.utilities import is_color_rgb +from .artist import Artist + + +class ShapeArtist(Artist): + """Base class for artists for geometric shapes. + + Parameters + ---------- + shape: :class:`compas.geometry.Shape` + The geometry of the shape. + color : tuple, optional + The RGB color. + + Class Attributes + ---------------- + default_color : tuple + The default rgb color value of the shape (``(255, 255, 255)``). + + Attributes + ---------- + shape: :class:`compas.geometry.Shape` + The geometry of the shape. + color : tuple + The RGB color. + u : int + The resolution in the u direction. + The default is ``16`` and the minimum ``3``. + v : int + The resolution in the v direction. + The default is ``16`` and the minimum ``3``. + """ + + default_color = (1, 1, 1) + + def __init__(self, shape, color=None, **kwargs): + super(ShapeArtist, self).__init__() + self._u = None + self._v = None + self._shape = None + self._color = None + self.shape = shape + self.color = color + self.u = kwargs.get('u') + self.v = kwargs.get('v') + + @property + def shape(self): + return self._shape + + @shape.setter + def shape(self, shape): + self._shape = shape + + @property + def color(self): + if not self._color: + self._color = self.default_color + return self._color + + @color.setter + def color(self, color): + if is_color_rgb(color): + self._color = color + + @property + def u(self): + if not self._u: + self._u = 16 + return self._u + + @u.setter + def u(self, u): + if u and u > 3: + self._u = u + + @property + def v(self): + if not self._v: + self._v = 16 + return self._v + + @v.setter + def v(self, v): + if v and v > 3: + self._v = v diff --git a/src/compas/artists/volmeshartist.py b/src/compas/artists/volmeshartist.py new file mode 100644 index 000000000000..0dd4d26acefe --- /dev/null +++ b/src/compas/artists/volmeshartist.py @@ -0,0 +1,364 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from abc import abstractmethod + +from compas.utilities import is_color_rgb +from .artist import Artist + + +class VolMeshArtist(Artist): + """Artist for drawing volmesh data structures. + + Parameters + ---------- + volmesh : :class:`compas.datastructures.VolMesh` + A COMPAS volmesh. + + Class Attributes + ---------------- + default_vertexcolor : tuple + The default color for vertices that do not have a specified color. + default_edgecolor : tuple + The default color for edges that do not have a specified color. + default_facecolor : tuple + The default color for faces that do not have a specified color. + default_cellcolor : tuple + The default color for cells that do not have a specified color. + + Attributes + ---------- + volmesh : :class:`compas.datastructures.VolMesh` + The COMPAS volmesh associated with the artist. + vertices : list + The list of vertices to draw. + Default is a list of all vertices of the volmesh. + edges : list + The list of edges to draw. + Default is a list of all edges of the volmesh. + faces : list + The list of faces to draw. + Default is a list of all faces of the volmesh. + cells : list + The list of cells to draw. + Default is a list of all cells of the volmesh. + vertex_xyz : dict[int, tuple(float, float, float)] + Mapping between vertices and their view coordinates. + The default view coordinates are the actual coordinates of the vertices of the volmesh. + vertex_color : dict[int, tuple(int, int, int)] + Mapping between vertices and RGB color values. + The colors have to be integer tuples with values in the range ``0-255``. + Missing vertices get the default vertex color (``~VolMeshArtist.default_vertexcolor``). + vertex_text : dict[int, str] + Mapping between vertices and text labels. + Missing vertices are labelled with the corresponding vertex identifiers. + edge_color : dict[tuple(int, int), tuple(int, int, int)] + Mapping between edges and RGB color values. + The colors have to be integer tuples with values in the range ``0-255``. + Missing edges get the default edge color (``~VolMeshArtist.default_edgecolor``). + edge_text : dict[tuple(int, int), str] + Mapping between edges and text labels. + Missing edges are labelled with the corresponding edge identifiers. + face_color : dict[tuple, tuple(int, int, int)] + Mapping between faces and RGB color values. + The colors have to be integer tuples with values in the range ``0-255``. + Missing faces get the default face color (``~VolMeshArtist.default_facecolor``). + face_text : dict[tuple, str] + Mapping between faces and text labels. + Missing faces are labelled with the corresponding face identifiers. + cell_color : dict[int, tuple(int, int, int)] + Mapping between cells and RGB color values. + The colors have to be integer tuples with values in the range ``0-255``. + Missing cells get the default cell color (``~VolMeshArtist.default_cellcolor``). + cell_text : dict[int, str] + Mapping between cells and text labels. + Missing cells are labelled with the corresponding cell identifiers. + + """ + + default_vertexcolor = (1, 1, 1) + default_edgecolor = (0, 0, 0) + default_facecolor = (0.8, 0.8, 0.8) + default_cellcolor = (1, 0, 0) + + def __init__(self, volmesh, **kwargs): + super(VolMeshArtist, self).__init__(**kwargs) + self._volmesh = None + self._vertices = None + self._edges = None + self._faces = None + self._cells = None + self._vertex_xyz = None + self._vertex_color = None + self._edge_color = None + self._face_color = None + self._cell_color = None + self._vertex_text = None + self._edge_text = None + self._face_text = None + self._cell_text = None + self.volmesh = volmesh + + @property + def volmesh(self): + return self._volmesh + + @volmesh.setter + def volmesh(self, volmesh): + self._volmesh = volmesh + self._vertex_xyz = None + + @property + def vertices(self): + if self._vertices is None: + self._vertices = list(self.volmesh.vertices()) + return self._vertices + + @vertices.setter + def vertices(self, vertices): + self._vertices = vertices + + @property + def edges(self): + if self._edges is None: + self._edges = list(self.volmesh.edges()) + return self._edges + + @edges.setter + def edges(self, edges): + self._edges = edges + + @property + def faces(self): + if self._faces is None: + self._faces = list(self.volmesh.faces()) + return self._faces + + @faces.setter + def faces(self, faces): + self._faces = faces + + @property + def cells(self): + if self._cells is None: + self._cells = list(self.volmesh.cells()) + return self._cells + + @cells.setter + def cells(self, cells): + self._cells = cells + + @property + def vertex_xyz(self): + if not self._vertex_xyz: + self._vertex_xyz = {vertex: self.volmesh.vertex_attributes(vertex, 'xyz') for vertex in self.volmesh.vertices()} + return self._vertex_xyz + + @vertex_xyz.setter + def vertex_xyz(self, vertex_xyz): + self._vertex_xyz = vertex_xyz + + @property + def vertex_color(self): + if not self._vertex_color: + self._vertex_color = {vertex: self.artist.default_vertexcolor for vertex in self.volmesh.vertices()} + return self._vertex_color + + @vertex_color.setter + def vertex_color(self, vertex_color): + if isinstance(vertex_color, dict): + self._vertex_color = vertex_color + elif is_color_rgb(vertex_color): + self._vertex_color = {vertex: vertex_color for vertex in self.volmesh.vertices()} + + @property + def edge_color(self): + if not self._edge_color: + self._edge_color = {edge: self.artist.default_edgecolor for edge in self.volmesh.edges()} + return self._edge_color + + @edge_color.setter + def edge_color(self, edge_color): + if isinstance(edge_color, dict): + self._edge_color = edge_color + elif is_color_rgb(edge_color): + self._edge_color = {edge: edge_color for edge in self.volmesh.edges()} + + @property + def face_color(self): + if not self._face_color: + self._face_color = {face: self.artist.default_facecolor for face in self.volmesh.faces()} + return self._face_color + + @face_color.setter + def face_color(self, face_color): + if isinstance(face_color, dict): + self._face_color = face_color + elif is_color_rgb(face_color): + self._face_color = {face: face_color for face in self.volmesh.faces()} + + @property + def cell_color(self): + if not self._cell_color: + self._cell_color = {cell: self.artist.default_cellcolor for cell in self.volmesh.cells()} + return self._cell_color + + @cell_color.setter + def cell_color(self, cell_color): + if isinstance(cell_color, dict): + self._cell_color = cell_color + elif is_color_rgb(cell_color): + self._cell_color = {cell: cell_color for cell in self.volmesh.cells()} + + @property + def vertex_text(self): + if not self._vertex_text: + self._vertex_text = {vertex: str(vertex) for vertex in self.volmesh.vertices()} + return self._vertex_text + + @vertex_text.setter + def vertex_text(self, text): + if text == 'key': + self._vertex_text = {vertex: str(vertex) for vertex in self.volmesh.vertices()} + elif text == 'index': + self._vertex_text = {vertex: str(index) for index, vertex in enumerate(self.volmesh.vertices())} + elif isinstance(text, dict): + self._vertex_text = text + + @property + def edge_text(self): + if not self._edge_text: + self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.volmesh.edges()} + return self._edge_text + + @edge_text.setter + def edge_text(self, text): + if text == 'key': + self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.volmesh.edges()} + elif text == 'index': + self._edge_text = {edge: str(index) for index, edge in enumerate(self.volmesh.edges())} + elif isinstance(text, dict): + self._edge_text = text + + @property + def face_text(self): + if not self._face_text: + self._face_text = {face: str(face) for face in self.volmesh.faces()} + return self._face_text + + @face_text.setter + def face_text(self, text): + if text == 'key': + self._face_text = {face: str(face) for face in self.volmesh.faces()} + elif text == 'index': + self._face_text = {face: str(index) for index, face in enumerate(self.volmesh.faces())} + elif isinstance(text, dict): + self._face_text = text + + @property + def cell_text(self): + if not self._cell_text: + self._cell_text = {cell: str(cell) for cell in self.volmesh.cells()} + return self._cell_text + + @cell_text.setter + def cell_text(self, text): + if text == 'key': + self._cell_text = {cell: str(cell) for cell in self.volmesh.cells()} + elif text == 'index': + self._cell_text = {cell: str(index) for index, cell in enumerate(self.volmesh.cells())} + elif isinstance(text, dict): + self._cell_text = text + + @abstractmethod + def draw_vertices(self, vertices=None, color=None, text=None): + """Draw the vertices of the mesh. + + Parameters + ---------- + vertices : list, optional + The vertices to include in the drawing. + Default is all vertices. + color : tuple or dict, optional + The color of the vertices, + as either a single color to be applied to all vertices, + or a color dict, mapping specific vertices to specific colors. + text : dict, optional + The text labels for the vertices + as a text dict, mapping specific vertices to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def draw_edges(self, edges=None, color=None, text=None): + """Draw the edges of the mesh. + + Parameters + ---------- + edges : list, optional + The edges to include in the drawing. + Default is all edges. + color : tuple or dict, optional + The color of the edges, + as either a single color to be applied to all edges, + or a color dict, mapping specific edges to specific colors. + text : dict, optional + The text labels for the edges + as a text dict, mapping specific edges to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def draw_faces(self, faces=None, color=None, text=None): + """Draw the faces of the mesh. + + Parameters + ---------- + faces : list, optional + The faces to include in the drawing. + Default is all faces. + color : tuple or dict, optional + The color of the faces, + as either a single color to be applied to all faces, + or a color dict, mapping specific faces to specific colors. + text : dict, optional + The text labels for the faces + as a text dict, mapping specific faces to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def draw_cells(self, cells=None, color=None, text=None): + """Draw the cells of the mesh. + + Parameters + ---------- + cells : list, optional + The cells to include in the drawing. + Default is all cells. + color : tuple or dict, optional + The color of the cells, + as either a single color to be applied to all cells, + or a color dict, mapping specific cells to specific colors. + text : dict, optional + The text labels for the cells + as a text dict, mapping specific cells to specific text labels. + """ + raise NotImplementedError + + @abstractmethod + def clear_vertices(self): + raise NotImplementedError + + @abstractmethod + def clear_edges(self): + raise NotImplementedError + + @abstractmethod + def clear_faces(self): + raise NotImplementedError + + @abstractmethod + def clear_cells(self): + raise NotImplementedError diff --git a/src/compas/plugins.py b/src/compas/plugins.py index 1c1f10db1ee1..964e92e9efe1 100644 --- a/src/compas/plugins.py +++ b/src/compas/plugins.py @@ -321,9 +321,11 @@ def plugin(method=None, category=None, requires=None, tryfirst=False, trylast=Fa The method to decorate as ``plugin``. category : str, optional An optional string to group or categorize plugins. - requires : list of str, optional - Optionally defines a list of packages that should be importable - for this plugin to be used. + requires : list, optional + Optionally defines a list of requirements that should be fulfilled + for this plugin to be used. The requirement can either be a package + name (``str``) or a ``callable`` with a boolean return value, + in which any arbitrary check can be implemented. tryfirst : bool, optional Plugins can declare a preferred priority by setting this to ``True``. By default ``False``. @@ -414,9 +416,16 @@ def check_importable(self, module_name): return self._cache[module_name] +def verify_requirement(manager, requirement): + if callable(requirement): + return requirement() + + return manager.importer.check_importable(requirement) + + def is_plugin_selectable(plugin, manager): if plugin.opts['requires']: - importable_requirements = (manager.importer.check_importable(name) for name in plugin.opts['requires']) + importable_requirements = (verify_requirement(manager, requirement) for requirement in plugin.opts['requires']) if not all(importable_requirements): if manager.DEBUG: diff --git a/src/compas_blender/__init__.py b/src/compas_blender/__init__.py index 31f96604336b..c28857dd0f25 100644 --- a/src/compas_blender/__init__.py +++ b/src/compas_blender/__init__.py @@ -26,6 +26,7 @@ def clear(): + """Clear all scene objects.""" # delete all objects bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=True, confirm=False) @@ -44,6 +45,11 @@ def clear(): bpy.data.collections.remove(block) +def redraw(): + """Trigger a redraw.""" + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + + __version__ = '1.8.1' @@ -84,6 +90,8 @@ def _get_default_blender_installation_path_windows(version): __all__ = [name for name in dir() if not name.startswith('_')] + __all_plugins__ = [ 'compas_blender.geometry.booleans', + 'compas_blender.artists', ] diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 86276095d274..dd2f95e4f939 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -8,40 +8,157 @@ Artists for visualising (painting) COMPAS data structures in Blender. -Base Classes -============ +Primitive Artists +================= .. autosummary:: :toctree: generated/ - BaseArtist + FrameArtist -Classes -======= +Shape Artists +============= .. autosummary:: :toctree: generated/ + :nosignatures: + + BoxArtist + CapsuleArtist + ConeArtist + CylinderArtist + SphereArtist + PolyhedronArtist + + +Datastructure Artists +===================== + +.. autosummary:: + :toctree: generated/ + :nosignatures: - FrameArtist NetworkArtist MeshArtist + + +Robot Artist +============ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + RobotModelArtist + +Base Classes +============ + +.. autosummary:: + :toctree: generated/ + + BlenderArtist + """ +import inspect + +import compas_blender -from ._artist import BaseArtist # noqa: F401 +from compas.plugins import plugin +from compas.artists import Artist +from compas.artists import DataArtistNotRegistered + +from compas.geometry import Box +from compas.geometry import Capsule +from compas.geometry import Cone +from compas.geometry import Cylinder +from compas.geometry import Frame +from compas.geometry import Polyhedron +from compas.geometry import Sphere +from compas.geometry import Torus +from compas.datastructures import Mesh +from compas.datastructures import Network +from compas.robots import RobotModel + +from .artist import BlenderArtist +from .boxartist import BoxArtist +from .capsuleartist import CapsuleArtist +from .coneartist import ConeArtist +from .cylinderartist import CylinderArtist from .frameartist import FrameArtist from .meshartist import MeshArtist from .networkartist import NetworkArtist -from .robotmodelartist import ( # noqa: F401 - BaseRobotModelArtist, - RobotModelArtist -) +from .polyhedronartist import PolyhedronArtist +from .robotmodelartist import RobotModelArtist +from .sphereartist import SphereArtist +from .torusartist import TorusArtist + + +@plugin(category='drawing-utils', pluggable_name='clear', requires=['bpy']) +def clear_blender(): + compas_blender.clear() + + +@plugin(category='drawing-utils', pluggable_name='redraw', requires=['bpy']) +def redraw_blender(): + compas_blender.redraw() + + +artists_registered = False + + +@plugin(category='factories', pluggable_name='new_artist', tryfirst=True, requires=['bpy']) +def new_artist_blender(cls, *args, **kwargs): + # "lazy registration" seems necessary to avoid item-artist pairs to be overwritten unintentionally + global artists_registered + + if not artists_registered: + BlenderArtist.register(Box, BoxArtist) + BlenderArtist.register(Capsule, CapsuleArtist) + BlenderArtist.register(Cone, ConeArtist) + BlenderArtist.register(Cylinder, CylinderArtist) + BlenderArtist.register(Frame, FrameArtist) + BlenderArtist.register(Mesh, MeshArtist) + BlenderArtist.register(Network, NetworkArtist) + BlenderArtist.register(Polyhedron, PolyhedronArtist) + BlenderArtist.register(RobotModel, RobotModelArtist) + BlenderArtist.register(Sphere, SphereArtist) + BlenderArtist.register(Torus, TorusArtist) + artists_registered = True + + data = args[0] + + if 'artist_type' in kwargs: + cls = kwargs['artist_type'] + else: + dtype = type(data) + if dtype not in BlenderArtist.ITEM_ARTIST: + raise DataArtistNotRegistered('No Blender artist is registered for this data type: {}'.format(dtype)) + cls = BlenderArtist.ITEM_ARTIST[dtype] + + # TODO: move this to the plugin module and/or to a dedicated function + + for name, value in inspect.getmembers(cls): + if inspect.isfunction(value): + if hasattr(value, '__isabstractmethod__'): + raise Exception('Abstract method not implemented: {}'.format(value)) + + return super(Artist, cls).__new__(cls) + __all__ = [ + 'BlenderArtist', + 'BoxArtist', + 'CapsuleArtist', + 'ConeArtist', + 'CylinderArtist', 'FrameArtist', - 'NetworkArtist', 'MeshArtist', - 'RobotModelArtist' + 'NetworkArtist', + 'PolyhedronArtist', + 'RobotModelArtist', + 'SphereArtist', + 'TorusArtist', ] diff --git a/src/compas_blender/artists/_artist.py b/src/compas_blender/artists/_artist.py deleted file mode 100644 index 46ba9aefb9a8..000000000000 --- a/src/compas_blender/artists/_artist.py +++ /dev/null @@ -1,76 +0,0 @@ -# from __future__ import annotations - -import bpy -import abc -import compas -import compas_blender - -from typing import Any, Type - - -__all__ = ['BaseArtist'] - - -_ITEM_ARTIST = {} - - -class BaseArtist(abc.ABC): - """Base class for all Blender artists. - - Attributes - ---------- - objects : list - A list of Blender objects (unique object names) created by the artist. - - """ - - def __init__(self): - self.objects = [] - - @staticmethod - def register(item_type: Type[compas.data.Data], artist_type: Type['BaseArtist']): - """Register a type of COMPAS object with a Blender artist. - - Parameters - ---------- - item_type : :class:`compas.data.Data` - artist_type : :class:`compas_blender.artists.BaseArtist` - - """ - _ITEM_ARTIST[item_type] = artist_type - - @staticmethod - def build(item: compas.data.Data, **kwargs: Any) -> 'BaseArtist': - """Build an artist corresponding to the item type. - - Parameters - ---------- - kwargs : dict, optional - The keyword arguments (kwargs) collected in a dict. - For relevant options, see the parameter lists of the matching artist type. - - Returns - ------- - :class:`compas_blender.artists.BaseArtist` - An artist of the type matching the provided item according to an item-artist map. - The map is created by registering item-artist type pairs using ``~BaseArtist.register``. - """ - artist_type = _ITEM_ARTIST[type(item)] - artist = artist_type(item, **kwargs) - return artist - - @abc.abstractmethod - def draw(self): - """Draw the item.""" - pass - - def redraw(self): - """Trigger a redraw.""" - bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) - - def clear(self): - """Delete all objects created by the artist.""" - if not self.objects: - return - compas_blender.delete_objects(self.objects) - self.objects = [] diff --git a/src/compas_blender/artists/artist.py b/src/compas_blender/artists/artist.py new file mode 100644 index 000000000000..a8fe7a50469e --- /dev/null +++ b/src/compas_blender/artists/artist.py @@ -0,0 +1,45 @@ +from typing import Union +from typing import Optional +from typing import Any + +import bpy +import compas_blender + +from compas.artists import Artist + + +class BlenderArtist(Artist): + """Base class for all Blender artists. + + Attributes + ---------- + objects : list + A list of Blender objects (unique object names) created by the artist. + + """ + + def __init__(self, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + + super().__init__(**kwargs) + + self._collection = None + self.collection = collection + + @property + def collection(self) -> bpy.types.Collection: + return self._collection + + @collection.setter + def collection(self, value: Union[str, bpy.types.Collection]): + if isinstance(value, bpy.types.Collection): + self._collection = value + elif isinstance(value, str): + self._collection = compas_blender.create_collection(value) + else: + raise Exception('Collection must be of type `str` or `bpy.types.Collection`.') + + def clear(self) -> None: + """Delete all objects created by the artist.""" + compas_blender.delete_objects(self.collection.objects) diff --git a/src/compas_blender/artists/boxartist.py b/src/compas_blender/artists/boxartist.py new file mode 100644 index 000000000000..7cad39fb2a4f --- /dev/null +++ b/src/compas_blender/artists/boxartist.py @@ -0,0 +1,46 @@ +from typing import Optional +from typing import Any +from typing import Union + +import bpy +import compas_blender +from compas.geometry import Box +from compas.artists import ShapeArtist +from .artist import BlenderArtist + + +class BoxArtist(BlenderArtist, ShapeArtist): + """Artist for drawing box shapes. + + Parameters + ---------- + box : :class:`compas.geometry.Box` + A COMPAS box. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + """ + + def __init__(self, + box: Box, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + + super().__init__(shape=box, collection=collection or box.name, **kwargs) + + def draw(self, color=None): + """Draw the box associated with the artist. + + Parameters + ---------- + color : tuple of float, optional + The RGB color of the box. + + Returns + ------- + list + The objects created in Blender. + """ + color = color or self.color + vertices, faces = self.shape.to_vertices_and_faces() + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection) + return [obj] diff --git a/src/compas_blender/artists/capsuleartist.py b/src/compas_blender/artists/capsuleartist.py new file mode 100644 index 000000000000..0921104b555c --- /dev/null +++ b/src/compas_blender/artists/capsuleartist.py @@ -0,0 +1,55 @@ +from typing import Optional +from typing import Any +from typing import Union + +import bpy + +import compas_blender +from compas.geometry import Capsule +from compas.artists import ShapeArtist +from .artist import BlenderArtist + + +class CapsuleArtist(BlenderArtist, ShapeArtist): + """Artist for drawing capsule shapes. + + Parameters + ---------- + capsule : :class:`compas.geometry.Capsule` + A COMPAS capsule. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + """ + + def __init__(self, + capsule: Capsule, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + + super().__init__(shape=capsule, collection=collection or capsule.name, **kwargs) + + def draw(self, color=None, u=None, v=None): + """Draw the capsule associated with the artist. + + Parameters + ---------- + color : tuple of float, optional + The RGB color of the capsule. + u : int, optional + Number of faces in the "u" direction. + Default is ``~CapsuleArtist.u``. + v : int, optional + Number of faces in the "v" direction. + Default is ``~CapsuleArtist.v``. + + Returns + ------- + list + The objects created in Blender. + """ + u = u or self.u + v = v or self.v + color = color or self.color + vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection) + return [obj] diff --git a/src/compas_blender/artists/coneartist.py b/src/compas_blender/artists/coneartist.py new file mode 100644 index 000000000000..41f968fbd079 --- /dev/null +++ b/src/compas_blender/artists/coneartist.py @@ -0,0 +1,51 @@ +from typing import Optional +from typing import Any +from typing import Union + +import bpy + +import compas_blender +from compas.geometry import Cone +from compas.artists import ShapeArtist +from .artist import BlenderArtist + + +class ConeArtist(BlenderArtist, ShapeArtist): + """Artist for drawing cone shapes. + + Parameters + ---------- + cone : :class:`compas.geometry.Cone` + A COMPAS cone. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + """ + + def __init__(self, + cone: Cone, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + + super().__init__(shape=cone, collection=collection or cone.name, **kwargs) + + def draw(self, color=None, u=None): + """Draw the cone associated with the artist. + + Parameters + ---------- + color : tuple of float, optional + The RGB color of the cone. + u : int, optional + Number of faces in the "u" direction. + Default is ``~ConeArtist.u``. + + Returns + ------- + list + The objects created in Blender. + """ + u = u or self.u + color = color or self.color + vertices, faces = self.shape.to_vertices_and_faces(u=u) + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection) + return [obj] diff --git a/src/compas_blender/artists/cylinderartist.py b/src/compas_blender/artists/cylinderartist.py new file mode 100644 index 000000000000..d74d4615dbe2 --- /dev/null +++ b/src/compas_blender/artists/cylinderartist.py @@ -0,0 +1,51 @@ +from typing import Optional +from typing import Any +from typing import Union + +import bpy + +import compas_blender +from compas.geometry import Cylinder +from compas.artists import ShapeArtist +from .artist import BlenderArtist + + +class CylinderArtist(BlenderArtist, ShapeArtist): + """Artist for drawing cylinder shapes. + + Parameters + ---------- + cylinder : :class:`compas.geometry.Cylinder` + A COMPAS cylinder. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + """ + + def __init__(self, + cylinder: Cylinder, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + + super().__init__(shape=cylinder, collection=collection or cylinder.name, **kwargs) + + def draw(self, color=None, u=None): + """Draw the cylinder associated with the artist. + + Parameters + ---------- + color : tuple of float, optional + The RGB color of the cylinder. + u : int, optional + Number of faces in the "u" direction. + Default is ``~CylinderArtist.u``. + + Returns + ------- + list + The objects created in Blender. + """ + u = u or self.u + color = color or self.color + vertices, faces = self.shape.to_vertices_and_faces(u=u) + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection) + return [obj] diff --git a/src/compas_blender/artists/frameartist.py b/src/compas_blender/artists/frameartist.py index 2e78aa271c1f..0d40ed8170e8 100644 --- a/src/compas_blender/artists/frameartist.py +++ b/src/compas_blender/artists/frameartist.py @@ -1,25 +1,26 @@ -import bpy from typing import List from typing import Optional +from typing import Any +from typing import Union + +import bpy from compas.geometry import Frame import compas_blender -from compas_blender.artists import BaseArtist - +from compas.artists import PrimitiveArtist +from .artist import BlenderArtist -__all__ = ['FrameArtist'] - -class FrameArtist(BaseArtist): +class FrameArtist(BlenderArtist, PrimitiveArtist): """Artist for drawing frames. Parameters ---------- frame: :class:`compas.geometry.Frame` A COMPAS frame. - collection: str - The name of the frame's collection. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. scale: float, optional Scale factor that controls the length of the axes. @@ -40,34 +41,15 @@ class FrameArtist(BaseArtist): Default is ``(0, 255, 0)``. color_zaxis : tuple of 3 int between 0 and 255 Default is ``(0, 0, 255)``. - - Examples - -------- - .. code-block:: python - - from compas.geometry import Pointcloud - from compas.geometry import Frame - - from compas_blender.artists import FrameArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]) - - - for point in pcl.points: - frame = tpl.copy() - frame.point = point - artist = FrameArtist(frame) - artist.draw() - """ def __init__(self, frame: Frame, - collection: Optional[bpy.types.Collection] = None, - scale: float = 1.0): - super(FrameArtist, self).__init__() - self.collection = collection - self.frame = frame + collection: Optional[Union[str, bpy.types.Collection]] = None, + scale: float = 1.0, + **kwargs: Any): + + super().__init__(primitive=frame, collection=collection or frame.name, **kwargs) + self.scale = scale or 1.0 self.color_origin = (0, 0, 0) self.color_xaxis = (255, 0, 0) @@ -98,9 +80,7 @@ def draw_origin(self) -> List[bpy.types.Object]: 'color': self.color_origin, 'radius': 0.01 }] - objects = compas_blender.draw_points(points, self.collection) - self.objects += objects - return objects + return compas_blender.draw_points(points, self.collection) def draw_axes(self) -> List[bpy.types.Object]: """Draw the axes of the frame. @@ -118,6 +98,4 @@ def draw_axes(self) -> List[bpy.types.Object]: {'start': origin, 'end': Y, 'color': self.color_yaxis, 'name': f"{self.frame.name}.yaxis"}, {'start': origin, 'end': Z, 'color': self.color_zaxis, 'name': f"{self.frame.name}.zaxis"}, ] - objects = compas_blender.draw_lines(lines, self.collection) - self.objects += objects - return objects + return compas_blender.draw_lines(lines, self.collection) diff --git a/src/compas_blender/artists/meshartist.py b/src/compas_blender/artists/meshartist.py index 56dc10951fb1..8acf734c0f05 100644 --- a/src/compas_blender/artists/meshartist.py +++ b/src/compas_blender/artists/meshartist.py @@ -1,9 +1,9 @@ -# from __future__ import annotations from typing import Dict from typing import List from typing import Optional from typing import Tuple from typing import Union +from typing import Any import bpy @@ -15,235 +15,161 @@ from compas.geometry import centroid_points from compas.geometry import scale_vector -from compas_blender.artists._artist import BaseArtist from compas.utilities import color_to_colordict +from compas.artists import MeshArtist +from .artist import BlenderArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=True) Color = Union[Tuple[int, int, int], Tuple[float, float, float]] -__all__ = ['MeshArtist'] - - -class MeshArtist(BaseArtist): +class MeshArtist(BlenderArtist, MeshArtist): """A mesh artist defines functionality for visualising COMPAS meshes in Blender. Parameters ---------- mesh : :class:`compas.datastructures.Mesh` A COMPAS mesh. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + vertices : list of int, optional + A list of vertex identifiers. + Default is ``None``, in which case all vertices are drawn. + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + faces : list, optional + A list of face identifiers. + The default is ``None``, in which case all faces are drawn. + vertexcolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the vertices. + edgecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + facecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the faces. + show_mesh : bool, optional + show_vertices : bool, optional + show_edges : bool, optional + show_faces : bool, optional Attributes ---------- - mesh : :class:`compas.datastructures.Mesh` - The COMPAS mesh associated with the artist. - - Examples - -------- - .. code-block:: python - - import compas - from compas.datastructures import Mesh - from compas_blender.artists import MeshArtist - - mesh = Mesh.from_obj(compas.get('faces.obj')) - - MeshArtist(mesh).draw() + vertexcollection : :class:`bpy.types.Collection` + The collection containing the vertices. + edgecollection : :class:`bpy.types.Collection` + The collection containing the edges. + facecollection : :class:`bpy.types.Collection` + The collection containing the faces. + vertexlabelcollection : :class:`bpy.types.Collection` + The collection containing the vertex labels. + edgelabelcollection : :class:`bpy.types.Collection` + The collection containing the edge labels. + facelabelcollection : :class:`bpy.types.Collection` + The collection containing the face labels. """ - def __init__(self, mesh: Mesh): - super().__init__() - self._collection = None - self._vertexcollection = None - self._edgecollection = None - self._facecollection = None - self._vertexnormalcollection = None - self._facenormalcollection = None - self._vertexlabelcollection = None - self._edgelabelcollection = None - self._facelabelcollection = None - self._object_vertex = {} - self._object_edge = {} - self._object_face = {} - self._object_vertexnormal = {} - self._object_facenormal = {} - self._object_vertexlabel = {} - self._object_edgelabel = {} - self._object_facelabel = {} - self.color_vertices = (1.0, 1.0, 1.0) - self.color_edges = (0.0, 0.0, 0.0) - self.color_faces = (0.7, 0.7, 0.7) - self.show_vertices = True - self.show_edges = True - self.show_faces = True - self.mesh = mesh - - @property - def collection(self) -> bpy.types.Collection: - if not self._collection: - self._collection = compas_blender.create_collection(self.mesh.name) - return self._collection + def __init__(self, + mesh: Mesh, + collection: Optional[Union[str, bpy.types.Collection]] = None, + vertices: Optional[List[int]] = None, + edges: Optional[List[int]] = None, + faces: Optional[List[int]] = None, + vertexcolor: Optional[Color] = None, + edgecolor: Optional[Color] = None, + facecolor: Optional[Color] = None, + show_mesh: bool = False, + show_vertices: bool = True, + show_edges: bool = True, + show_faces: bool = True, + **kwargs: Any): + + super().__init__(mesh=mesh, collection=collection or mesh.name, **kwargs) + + self.vertices = vertices + self.edges = edges + self.faces = faces + self.vertex_color = vertexcolor + self.edge_color = edgecolor + self.face_color = facecolor + self.show_mesh = show_mesh + self.show_vertices = show_vertices + self.show_edges = show_edges + self.show_faces = show_faces @property def vertexcollection(self) -> bpy.types.Collection: - path = f"{self.mesh.name}::Vertices" if not self._vertexcollection: - self._vertexcollection = compas_blender.create_collections_from_path(path)[1] + self._vertexcollection = compas_blender.create_collection('Vertices', parent=self.collection) return self._vertexcollection @property def edgecollection(self) -> bpy.types.Collection: - path = f"{self.mesh.name}::Edges" if not self._edgecollection: - self._edgecollection = compas_blender.create_collections_from_path(path)[1] + self._edgecollection = compas_blender.create_collection('Edges', parent=self.collection) return self._edgecollection @property def facecollection(self) -> bpy.types.Collection: - path = f"{self.mesh.name}::Faces" if not self._facecollection: - self._facecollection = compas_blender.create_collections_from_path(path)[1] + self._facecollection = compas_blender.create_collection('Faces', parent=self.collection) return self._facecollection @property def vertexnormalcollection(self) -> bpy.types.Collection: - path = f"{self.mesh.name}::VertexNormals" if not self._vertexnormalcollection: - self._vertexnormalcollection = compas_blender.create_collections_from_path(path)[1] + self._vertexnormalcollection = compas_blender.create_collection('VertexNormals', parent=self.collection) return self._vertexnormalcollection @property def facenormalcollection(self) -> bpy.types.Collection: - path = f"{self.mesh.name}::FaceNormals" if not self._facenormalcollection: - self._facenormalcollection = compas_blender.create_collections_from_path(path)[1] + self._facenormalcollection = compas_blender.create_collection('FaceNormals', parent=self.collection) return self._facenormalcollection @property def vertexlabelcollection(self) -> bpy.types.Collection: - path = f"{self.mesh.name}::VertexLabels" if not self._vertexlabelcollection: - self._vertexlabelcollection = compas_blender.create_collections_from_path(path)[1] + self._vertexlabelcollection = compas_blender.create_collection('VertexLabels', parent=self.collection) return self._vertexlabelcollection @property def edgelabelcollection(self) -> bpy.types.Collection: - path = f"{self.mesh.name}::EdgeLabels" if not self._edgelabelcollection: - self._edgelabelcollection = compas_blender.create_collections_from_path(path)[1] + self._edgelabelcollection = compas_blender.create_collection('EdgeLabels', parent=self.collection) return self._edgelabelcollection @property def facelabelcollection(self) -> bpy.types.Collection: - path = f"{self.mesh.name}::FaceLabels" if not self._facelabelcollection: - self._facelabelcollection = compas_blender.create_collections_from_path(path)[1] + self._facelabelcollection = compas_blender.create_collection('FaceLabels', parent=self.collection) return self._facelabelcollection - @property - def object_vertex(self) -> Dict[bpy.types.Object, int]: - """Map between Blender object objects and mesh vertex identifiers.""" - return self._object_vertex - - @object_vertex.setter - def object_vertex(self, values): - self._object_vertex = dict(values) - - @property - def object_edge(self) -> Dict[bpy.types.Object, Tuple[int, int]]: - """Map between Blender object objects and mesh edge identifiers.""" - return self._object_edge - - @object_edge.setter - def object_edge(self, values): - self._object_edge = dict(values) - - @property - def object_face(self) -> Dict[bpy.types.Object, int]: - """Map between Blender object objects and mesh face identifiers.""" - return self._object_face - - @object_face.setter - def object_face(self, values): - self._object_face = dict(values) - - @property - def object_vertexnormal(self) -> Dict[bpy.types.Object, int]: - """Map between Blender object objects and mesh vertex normal identifiers.""" - return self._object_vertexnormal - - @object_vertexnormal.setter - def object_vertexnormal(self, values): - self._object_vertexnormal = dict(values) - - @property - def object_facenormal(self) -> Dict[bpy.types.Object, int]: - """Map between Blender object objects and mesh face normal identifiers.""" - return self._object_facenormal - - @object_facenormal.setter - def object_facenormal(self, values): - self._object_facenormal = dict(values) - - @property - def object_vertexlabel(self) -> Dict[bpy.types.Object, int]: - """Map between Blender object objects and mesh vertex label identifiers.""" - return self._object_vertexlabel - - @object_vertexlabel.setter - def object_vertexlabel(self, values): - self._object_vertexlabel = dict(values) - - @property - def object_edgelabel(self) -> Dict[bpy.types.Object, Tuple[int, int]]: - """Map between Blender object objects and mesh edge label identifiers.""" - return self._object_edgelabel - - @object_edgelabel.setter - def object_edgelabel(self, values): - self._object_edgelabel = dict(values) - - @property - def object_facelabel(self) -> Dict[bpy.types.Object, int]: - """Map between Blender object objects and mesh face label identifiers.""" - return self._object_facelabel - - @object_facelabel.setter - def object_facelabel(self, values): - self._object_facelabel = dict(values) - # ========================================================================== # clear # ========================================================================== - def clear(self) -> None: - """Clear all objects previously drawn by this artist. - """ - objects = [] - objects += list(self.object_vertex) - objects += list(self.object_edge) - objects += list(self.object_face) - objects += list(self.object_vertexnormal) - objects += list(self.object_facenormal) - objects += list(self.object_vertexlabel) - objects += list(self.object_edgelabel) - objects += list(self.object_facelabel) - compas_blender.delete_objects(objects, purge_data=True) - self._object_vertex = {} - self._object_edge = {} - self._object_face = {} - self._object_vertexnormal = {} - self._object_facenormal = {} - self._object_vertexlabel = {} - self._object_edgelabel = {} - self._object_facelabel = {} + def clear_vertices(self): + compas_blender.delete_objects(self.vertexcollection.objects) + + def clear_edges(self): + compas_blender.delete_objects(self.edgecollection.objects) + + def clear_faces(self): + compas_blender.delete_objects(self.facecollection.objects) # ========================================================================== - # components + # draw # ========================================================================== - def draw(self) -> None: + def draw(self, + vertices: Optional[List[int]] = None, + edges: Optional[List[Tuple[int, int]]] = None, + faces: Optional[List[int]] = None, + vertexcolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + edgecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + facecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None + ) -> None: """Draw the mesh using the chosen visualisation settings. Parameters @@ -253,12 +179,14 @@ def draw(self) -> None: """ self.clear() + if self.show_mesh: + self.draw_mesh() if self.show_vertices: - self.draw_vertices() - if self.show_faces: - self.draw_faces() + self.draw_vertices(vertices=vertices, color=vertexcolor) if self.show_edges: - self.draw_edges() + self.draw_edges(edges=edges, color=edgecolor) + if self.show_faces: + self.draw_faces(faces=faces, color=facecolor) def draw_mesh(self) -> List[bpy.types.Object]: """Draw the mesh.""" @@ -268,7 +196,8 @@ def draw_mesh(self) -> List[bpy.types.Object]: def draw_vertices(self, vertices: Optional[List[int]] = None, - color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> List[bpy.types.Object]: + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None + ) -> List[bpy.types.Object]: """Draw a selection of vertices. Parameters @@ -282,56 +211,23 @@ def draw_vertices(self, Returns ------- list of :class:`bpy.types.Object` - """ - vertices = vertices or list(self.mesh.vertices()) - vertex_color = colordict(color, vertices, default=self.color_vertices) + self.vertex_color = color + vertices = vertices or self.vertices points = [] for vertex in vertices: points.append({ - 'pos': self.mesh.vertex_coordinates(vertex), + 'pos': self.vertex_xyz[vertex], 'name': f"{self.mesh.name}.vertex.{vertex}", - 'color': vertex_color[vertex], + 'color': self.vertex_color.get(vertex, self.default_vertexcolor), 'radius': 0.01 }) - objects = compas_blender.draw_points(points, self.vertexcollection) - self.object_vertex = zip(objects, vertices) - return objects - - def draw_faces(self, - faces: Optional[List[int]] = None, - color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> List[bpy.types.Object]: - """Draw a selection of faces. - - Parameters - ---------- - faces : list - A list of face keys identifying which faces to draw. - The default is ``None``, in which case all faces are drawn. - color : rgb-tuple or dict of rgb-tuple - The color specification for the faces. - - Returns - ------- - list of :class:`bpy.types.Object` - - """ - faces = faces or list(self.mesh.faces()) - face_color = colordict(color, faces, default=self.color_faces) - facets = [] - for face in faces: - facets.append({ - 'points': self.mesh.face_coordinates(face), - 'name': f"{self.mesh.name}.face.{face}", - 'color': face_color[face] - }) - objects = compas_blender.draw_faces(facets, self.facecollection) - self.object_face = zip(objects, faces) - return objects + return compas_blender.draw_points(points, self.vertexcollection) def draw_edges(self, edges: Optional[List[Tuple[int, int]]] = None, - color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> List[bpy.types.Object]: + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None + ) -> List[bpy.types.Object]: """Draw a selection of edges. Parameters @@ -347,19 +243,46 @@ def draw_edges(self, list of :class:`bpy.types.Object` """ - edges = edges or list(self.mesh.edges()) - edge_color = colordict(color, edges, default=self.color_edges) + self.edge_color = color + edges = edges or self.edges lines = [] for edge in edges: lines.append({ - 'start': self.mesh.vertex_coordinates(edge[0]), - 'end': self.mesh.vertex_coordinates(edge[1]), - 'color': edge_color[edge], + 'start': self.vertex_xyz[edge[0]], + 'end': self.vertex_xyz[edge[1]], + 'color': self.edge_color.get(edge, self.default_edgecolor), 'name': f"{self.mesh.name}.edge.{edge[0]}-{edge[1]}" }) - objects = compas_blender.draw_lines(lines, self.edgecollection) - self.object_edge = zip(objects, edges) - return objects + return compas_blender.draw_lines(lines, self.edgecollection) + + def draw_faces(self, + faces: Optional[List[int]] = None, + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None + ) -> List[bpy.types.Object]: + """Draw a selection of faces. + + Parameters + ---------- + faces : list + A list of face keys identifying which faces to draw. + The default is ``None``, in which case all faces are drawn. + color : rgb-tuple or dict of rgb-tuple + The color specification for the faces. + + Returns + ------- + list of :class:`bpy.types.Object` + """ + self.face_color = color + faces = faces or self.faces + facets = [] + for face in faces: + facets.append({ + 'points': [self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)], + 'name': f"{self.mesh.name}.face.{face}", + 'color': self.face_color.get(face, self.default_facecolor) + }) + return compas_blender.draw_faces(facets, self.facecollection) # ========================================================================== # draw normals @@ -387,11 +310,11 @@ def draw_vertexnormals(self, ------- list of :class:`bpy.types.Object` """ - vertices = vertices or list(self.mesh.vertices()) + vertices = vertices or self.vertices vertex_color = colordict(color, vertices, default=(0., 1., 0.)) lines = [] for vertex in vertices: - a = self.mesh.vertex_coordinates(vertex) + a = self.vertex_xyz[vertex] n = self.mesh.vertex_normal(vertex) b = add_vectors(a, scale_vector(n, scale)) lines.append({ @@ -400,9 +323,7 @@ def draw_vertexnormals(self, 'color': vertex_color[vertex], 'name': "{}.vertexnormal.{}".format(self.mesh.name, vertex) }) - objects = compas_blender.draw_lines(lines, collection=self.vertexnormalcollection) - self.object_vertexnormal = zip(objects, vertices) - return objects + return compas_blender.draw_lines(lines, collection=self.vertexnormalcollection) def draw_facenormals(self, faces: Optional[List[List[int]]] = None, @@ -426,13 +347,11 @@ def draw_facenormals(self, ------- list of :class:`bpy.types.Object` """ - faces = faces or list(self.mesh.faces()) + faces = faces or self.faces face_color = colordict(color, faces, default=(0., 1., 1.)) lines = [] for face in faces: - a = centroid_points( - [self.mesh.vertex_coordinates(vertex) for vertex in self.mesh.face_vertices(face)] - ) + a = centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]) n = self.mesh.face_normal(face) b = add_vectors(a, scale_vector(n, scale)) lines.append({ @@ -441,9 +360,7 @@ def draw_facenormals(self, 'name': "{}.facenormal.{}".format(self.mesh.name, face), 'color': face_color[face] }) - objects = compas_blender.draw_lines(lines, collection=self.facenormalcollection) - self.object_facenormal = zip(objects, faces) - return objects + return compas_blender.draw_lines(lines, collection=self.facenormalcollection) # ========================================================================== # draw labels @@ -468,24 +385,23 @@ def draw_vertexlabels(self, list of :class:`bpy.types.Object` """ if not text or text == 'key': - vertex_text = {vertex: str(vertex) for vertex in self.mesh.vertices()} + vertex_text = {vertex: str(vertex) for vertex in self.vertices} elif text == 'index': - vertex_text = {vertex: str(index) for index, vertex in enumerate(self.mesh.vertices())} + vertex_text = {vertex: str(index) for index, vertex in enumerate(self.vertices)} elif isinstance(text, dict): vertex_text = text else: raise NotImplementedError - vertex_color = colordict(color, vertex_text, default=self.color_vertices) + vertex_color = colordict(color, vertex_text, default=self.default_vertexcolor) labels = [] for vertex in vertex_text: labels.append({ - 'pos': self.mesh.vertex_coordinates(vertex), + 'pos': self.vertex_xyz[vertex], 'name': "{}.vertexlabel.{}".format(self.mesh.name, vertex), 'text': vertex_text[vertex], - 'color': vertex_color[vertex]}) - objects = compas_blender.draw_texts(labels, collection=self.vertexlabelcollection) - self.object_vertexlabel = zip(objects, vertex_text) - return objects + 'color': vertex_color[vertex] + }) + return compas_blender.draw_texts(labels, collection=self.vertexlabelcollection) def draw_edgelabels(self, text: Optional[Dict[Tuple[int, int], str]] = None, @@ -506,23 +422,20 @@ def draw_edgelabels(self, list of :class:`bpy.types.Object` """ if text is None: - edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.mesh.edges()} + edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.edges} elif isinstance(text, dict): edge_text = text else: raise NotImplementedError - edge_color = colordict(color, edge_text, default=self.color_edges) + edge_color = colordict(color, edge_text, default=self.default_edgecolor) labels = [] for edge in edge_text: labels.append({ - 'pos': centroid_points( - [self.mesh.vertex_coordinates(edge[0]), self.mesh.vertex_coordinates(edge[1])] - ), + 'pos': centroid_points([self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]]), 'name': "{}.edgelabel.{}-{}".format(self.mesh.name, *edge), - 'text': edge_text[edge]}) - objects = compas_blender.draw_texts(labels, collection=self.edgelabelcollection, color=edge_color) - self.object_edgelabel = zip(objects, edge_text) - return objects + 'text': edge_text[edge] + }) + return compas_blender.draw_texts(labels, collection=self.edgelabelcollection, color=edge_color) def draw_facelabels(self, text: Optional[Dict[int, str]] = None, @@ -543,22 +456,19 @@ def draw_facelabels(self, list of :class:`bpy.types.Object` """ if not text or text == 'key': - face_text = {face: str(face) for face in self.mesh.faces()} + face_text = {face: str(face) for face in self.faces} elif text == 'index': - face_text = {face: str(index) for index, face in enumerate(self.mesh.faces())} + face_text = {face: str(index) for index, face in enumerate(self.faces)} elif isinstance(text, dict): face_text = text else: raise NotImplementedError - face_color = color or self.color_faces + face_color = color or self.default_facecolor labels = [] for face in face_text: labels.append({ - 'pos': centroid_points( - [self.mesh.vertex_coordinates(vertex) for vertex in self.mesh.face_vertices(face)] - ), + 'pos': centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]), 'name': "{}.facelabel.{}".format(self.mesh.name, face), - 'text': face_text[face]}) - objects = compas_blender.draw_texts(labels, collection=self.collection, color=face_color) - self.object_facelabel = zip(objects, face_text) - return objects + 'text': face_text[face] + }) + return compas_blender.draw_texts(labels, collection=self.collection, color=face_color) diff --git a/src/compas_blender/artists/networkartist.py b/src/compas_blender/artists/networkartist.py index 9aa344d5a086..8c96cbd02dfe 100644 --- a/src/compas_blender/artists/networkartist.py +++ b/src/compas_blender/artists/networkartist.py @@ -1,160 +1,153 @@ -# from __future__ import annotations from typing import Dict from typing import List from typing import Optional from typing import Tuple from typing import Union +from typing import Any import bpy from functools import partial import compas_blender -from compas_blender.artists._artist import BaseArtist from compas.datastructures import Network from compas.geometry import centroid_points from compas.utilities import color_to_colordict +from compas.artists import NetworkArtist +from .artist import BlenderArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=True) Color = Union[Tuple[int, int, int], Tuple[float, float, float]] -__all__ = [ - 'NetworkArtist', -] - - -class NetworkArtist(BaseArtist): +class NetworkArtist(BlenderArtist, NetworkArtist): """Artist for COMPAS network objects. Parameters ---------- network : :class:`compas.datastructures.Network` A COMPAS network. - settings : dict, optional - A dict with custom visualisation settings. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + nodes : list of int, optional + A list of node identifiers. + Default is ``None``, in which case all nodes are drawn. + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + nodecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the nodes. + edgecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + show_nodes : bool, optional + show_edges : bool, optional Attributes ---------- - network : :class:`compas.datastructures.Network` - The COMPAS network associated with the artist. - settings : dict - Default settings for color, scale, tolerance, ... - + nodecollection : :class:`bpy.types.Collection` + The collection containing the nodes. + edgecollection : :class:`bpy.types.Collection` + The collection containing the edges. + nodelabelcollection : :class:`bpy.types.Collection` + The collection containing the node labels. + edgelabelcollection : :class:`bpy.types.Collection` + The collection containing the edge labels. """ - def __init__(self, network: Network): - super().__init__() - self._nodecollection = None - self._edgecollection = None - self._nodelabelcollection = None - self._edgelabelcollection = None - self._object_node = {} - self._object_edge = {} - self._object_nodelabel = {} - self._object_edgelabel = {} - self.color_nodes = (1.0, 1.0, 1.0) - self.color_edges = (0.0, 0.0, 0.0) - self.show_nodes = True, - self.show_edges = True, - self.show_nodelabels = False, - self.show_edgelabels = False - self.network = network + def __init__(self, + network: Network, + collection: Optional[Union[str, bpy.types.Collection]] = None, + nodes: Optional[List[int]] = None, + edges: Optional[List[int]] = None, + nodecolor: Color = (1, 1, 1), + edgecolor: Color = (0, 0, 0), + show_nodes: bool = True, + show_edges: bool = True, + **kwargs: Any): + + super().__init__(network=network, collection=collection or network.name, **kwargs) + + self.nodes = nodes + self.edges = edges + self.node_color = nodecolor + self.edge_color = edgecolor + self.show_nodes = show_nodes + self.show_edges = show_edges @property def nodecollection(self) -> bpy.types.Collection: - path = f"{self.network.name}::Nodes" if not self._nodecollection: - self._nodecollection = compas_blender.create_collections_from_path(path)[1] + self._nodecollection = compas_blender.create_collection('Nodes', parent=self.collection) return self._nodecollection @property def edgecollection(self) -> bpy.types.Collection: - path = f"{self.network.name}::Edges" if not self._edgecollection: - self._edgecollection = compas_blender.create_collections_from_path(path)[1] + self._edgecollection = compas_blender.create_collection('Edges', parent=self.collection) return self._edgecollection @property def nodelabelcollection(self) -> bpy.types.Collection: - path = f"{self.network.name}::VertexLabels" if not self._nodelabelcollection: - self._nodelabelcollection = compas_blender.create_collections_from_path(path)[1] + self._nodelabelcollection = compas_blender.create_collection('NodeLabels', parent=self.collection) return self._nodelabelcollection @property def edgelabelcollection(self) -> bpy.types.Collection: - path = f"{self.network.name}::EdgeLabels" if not self._edgelabelcollection: - self._edgelabelcollection = compas_blender.create_collections_from_path(path)[1] + self._edgelabelcollection = compas_blender.create_collection('EdgeLabels', parent=self.collection) return self._edgelabelcollection - @property - def object_node(self) -> Dict[bpy.types.Object, int]: - if not self._object_node: - self._object_node = {} - return self._object_node + # ========================================================================== + # clear + # ========================================================================== - @object_node.setter - def object_node(self, values): - self._object_node = dict(values) + def clear_nodes(self): + compas_blender.delete_objects(self.nodecollection.objects) - @property - def object_edge(self) -> Dict[bpy.types.Object, Tuple[int, int]]: - if not self._object_edge: - self._object_edge = {} - return self._object_edge + def clear_edges(self): + compas_blender.delete_objects(self.edgecollection.objects) - @object_edge.setter - def object_edge(self, values): - self._object_edge = dict(values) + def clear_nodelabels(self): + compas_blender.delete_objects(self.nodelabelcollection.objects) - @property - def object_nodelabel(self) -> Dict[bpy.types.Object, int]: - """Map between Blender object objects and node label identifiers.""" - return self._object_nodelabel + def clear_edgelabels(self): + compas_blender.delete_objects(self.edgelabelcollection.objects) - @object_nodelabel.setter - def object_nodelabel(self, values): - self._object_nodelabel = dict(values) + # ========================================================================== + # draw + # ========================================================================== - @property - def object_edgelabel(self) -> Dict[bpy.types.Object, Tuple[int, int]]: - """Map between Blender object objects and edge label identifiers.""" - return self._object_edgelabel - - @object_edgelabel.setter - def object_edgelabel(self, values): - self._object_edgelabel = dict(values) - - def clear(self) -> None: - objects = list(self.object_node) - objects += list(self.object_edge) - objects += list(self.object_nodelabel) - objects += list(self.object_edgelabel) - compas_blender.delete_objects(objects, purge_data=True) - self._object_node = {} - self._object_edge = {} - self._object_nodelabel = {} - self._object_edgelabel = {} - - def draw(self) -> None: + def draw(self, + nodes: Optional[List[int]] = None, + edges: Optional[Tuple[int, int]] = None, + nodecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + edgecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None + ) -> None: """Draw the network. - Returns - ------- - list of :class:`bpy.types.Object` - The created Blender objects. - + Parameters + ---------- + nodes : list of int, optional + A list of node identifiers. + Default is ``None``, in which case all nodes are drawn. + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + nodecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the nodes. + edgecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. """ self.clear() if self.show_nodes: - self.draw_nodes() + self.draw_nodes(nodes=nodes, color=nodecolor) if self.show_edges: - self.draw_edges() + self.draw_edges(edges=edges, color=edgecolor) def draw_nodes(self, nodes: Optional[List[int]] = None, - color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> List[bpy.types.Object]: + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None + ) -> List[bpy.types.Object]: """Draw a selection of nodes. Parameters @@ -170,22 +163,22 @@ def draw_nodes(self, list of :class:`bpy.types.Object` """ - nodes = nodes or list(self.network.nodes()) - node_color = colordict(color, nodes, default=self.color_nodes) + self.node_color = color + nodes = nodes or self.nodes points = [] for node in nodes: points.append({ - 'pos': self.network.node_coordinates(node), + 'pos': self.node_xyz[node], 'name': f"{self.network.name}.node.{node}", - 'color': node_color[node], - 'radius': 0.05}) - objects = compas_blender.draw_points(points, self.nodecollection) - self.object_node = zip(objects, nodes) - return objects + 'color': self.node_color.get(node, self.default_nodecolor), + 'radius': 0.05 + }) + return compas_blender.draw_points(points, self.nodecollection) def draw_edges(self, edges: Optional[Tuple[int, int]] = None, - color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> List[bpy.types.Object]: + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None + ) -> List[bpy.types.Object]: """Draw a selection of edges. Parameters @@ -201,19 +194,18 @@ def draw_edges(self, list of :class:`bpy.types.Object` """ - edges = edges or list(self.network.edges()) - edge_color = colordict(color, edges, default=self.color_edges) + self.edge_color = color + edges = edges or self.edges lines = [] for edge in edges: lines.append({ - 'start': self.network.node_coordinates(edge[0]), - 'end': self.network.node_coordinates(edge[1]), - 'color': edge_color[edge], + 'start': self.node_xyz[edge[0]], + 'end': self.node_xyz[edge[1]], + 'color': self.edge_color.get(edge, self.default_edgecolor), 'name': f"{self.network.name}.edge.{edge[0]}-{edge[1]}", - 'width': 0.02}) - objects = compas_blender.draw_lines(lines, self.edgecollection) - self.object_edge = zip(objects, edges) - return objects + 'width': 0.02 + }) + return compas_blender.draw_lines(lines, self.edgecollection) def draw_nodelabels(self, text: Optional[Dict[int, str]] = None, @@ -235,24 +227,23 @@ def draw_nodelabels(self, list of :class:`bpy.types.Object` """ if not text or text == 'key': - node_text = {vertex: str(vertex) for vertex in self.network.nodes()} + node_text = {vertex: str(vertex) for vertex in self.nodes} elif text == 'index': - node_text = {vertex: str(index) for index, vertex in enumerate(self.network.nodes())} + node_text = {vertex: str(index) for index, vertex in enumerate(self._nodes)} elif isinstance(text, dict): node_text = text else: raise NotImplementedError - node_color = colordict(color, node_text, default=self.color_nodes) + node_color = colordict(color, node_text, default=self.default_nodecolor) labels = [] for node in node_text: labels.append({ - 'pos': self.network.node_coordinates(node), + 'pos': self.node_xyz[node], 'name': "{}.nodelabel.{}".format(self.network.name, node), 'text': node_text[node], - 'color': node_color[node]}) - objects = compas_blender.draw_texts(labels, collection=self.nodelabelcollection) - self.object_nodelabel = zip(objects, node_text) - return objects + 'color': node_color[node] + }) + return compas_blender.draw_texts(labels, collection=self.nodelabelcollection) def draw_edgelabels(self, text: Optional[Dict[Tuple[int, int], str]] = None, @@ -274,20 +265,17 @@ def draw_edgelabels(self, list of :class:`bpy.types.Object` """ if text is None: - edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.network.edges()} + edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.edges} elif isinstance(text, dict): edge_text = text else: raise NotImplementedError - edge_color = colordict(color, edge_text, default=self.color_edges) + edge_color = colordict(color, edge_text, default=self.default_edgecolor) labels = [] for edge in edge_text: labels.append({ - 'pos': centroid_points( - [self.network.node_coordinates(edge[0]), self.network.node_coordinates(edge[1])] - ), + 'pos': centroid_points([self.node_xyz[edge[0]], self.node_xyz[edge[1]]]), 'name': "{}.edgelabel.{}-{}".format(self.network.name, *edge), - 'text': edge_text[edge]}) - objects = compas_blender.draw_texts(labels, collection=self.edgelabelcollection, color=edge_color) - self.object_edgelabel = zip(objects, edge_text) - return objects + 'text': edge_text[edge] + }) + return compas_blender.draw_texts(labels, collection=self.edgelabelcollection, color=edge_color) diff --git a/src/compas_blender/artists/polyhedronartist.py b/src/compas_blender/artists/polyhedronartist.py new file mode 100644 index 000000000000..79561ea07bc3 --- /dev/null +++ b/src/compas_blender/artists/polyhedronartist.py @@ -0,0 +1,46 @@ +from typing import Optional +from typing import Any +from typing import Union + +import bpy +import compas_blender +from compas.geometry import Polyhedron +from compas.artists import ShapeArtist +from .artist import BlenderArtist + + +class PolyhedronArtist(BlenderArtist, ShapeArtist): + """Artist for drawing polyhedron shapes. + + Parameters + ---------- + polyhedron : :class:`compas.geometry.Polyhedron` + A COMPAS polyhedron. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + """ + + def __init__(self, + polyhedron: Polyhedron, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + + super().__init__(shape=polyhedron, collection=collection or polyhedron.name, **kwargs) + + def draw(self, color=None): + """Draw the polyhedron associated with the artist. + + Parameters + ---------- + color : tuple of float, optional + The RGB color of the polyhedron. + + Returns + ------- + list + The objects created in Blender. + """ + color = color or self.color + vertices, faces = self.shape.to_vertices_and_faces() + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection) + return [obj] diff --git a/src/compas_blender/artists/robotmodelartist.py b/src/compas_blender/artists/robotmodelartist.py index 9bbf94f1e7af..f5167e0ad35d 100644 --- a/src/compas_blender/artists/robotmodelartist.py +++ b/src/compas_blender/artists/robotmodelartist.py @@ -1,4 +1,7 @@ -from typing import Union, Tuple +from typing import Union +from typing import Tuple +from typing import Optional +from typing import Any import bpy import mathutils @@ -7,40 +10,26 @@ from compas.datastructures import Mesh from compas.geometry import Transformation, Shape from compas.robots import RobotModel -from compas.robots.base_artist import BaseRobotModelArtist +from compas.artists import RobotModelArtist +from .artist import BlenderArtist -__all__ = [ - 'RobotModelArtist', -] - -class RobotModelArtist(BaseRobotModelArtist): +class RobotModelArtist(BlenderArtist, RobotModelArtist): """Visualizer for robot models inside a Blender environment. Parameters ---------- model : :class:`compas.robots.RobotModel` Robot model. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. """ def __init__(self, model: RobotModel, - collection: bpy.types.Collection = None): - self.collection = collection or model.name - super(RobotModelArtist, self).__init__(model) - - @property - def collection(self) -> bpy.types.Collection: - return self._collection - - @collection.setter - def collection(self, value: Union[str, bpy.types.Collection]): - if isinstance(value, bpy.types.Collection): - self._collection = value - elif isinstance(value, str): - self._collection = compas_blender.create_collection(value) - else: - raise Exception('Collection must be of type `str` or `bpy.types.Collection`.') + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + super().__init__(model=model, collection=collection or model.name, **kwargs) def transform(self, native_mesh: bpy.types.Object, transformation: Transformation) -> None: native_mesh.matrix_world = mathutils.Matrix(transformation.matrix) @ native_mesh.matrix_world diff --git a/src/compas_blender/artists/sphereartist.py b/src/compas_blender/artists/sphereartist.py new file mode 100644 index 000000000000..f14edb4d9c25 --- /dev/null +++ b/src/compas_blender/artists/sphereartist.py @@ -0,0 +1,55 @@ +from typing import Optional +from typing import Any +from typing import Union + +import bpy + +import compas_blender +from compas.geometry import Sphere +from compas.artists import ShapeArtist +from .artist import BlenderArtist + + +class SphereArtist(BlenderArtist, ShapeArtist): + """Artist for drawing sphere shapes. + + Parameters + ---------- + sphere : :class:`compas.geometry.Sphere` + A COMPAS sphere. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + """ + + def __init__(self, + sphere: Sphere, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + + super().__init__(shape=sphere, collection=collection or sphere.name, **kwargs) + + def draw(self, color=None, u=None, v=None): + """Draw the sphere associated with the artist. + + Parameters + ---------- + color : tuple of float, optional + The RGB color of the sphere. + u : int, optional + Number of faces in the "u" direction. + Default is ``~SphereArtist.u``. + v : int, optional + Number of faces in the "v" direction. + Default is ``~SphereArtist.v``. + + Returns + ------- + list + The objects created in Blender. + """ + u = u or self.u + v = v or self.v + color = color or self.color + vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection) + return [obj] diff --git a/src/compas_blender/artists/torusartist.py b/src/compas_blender/artists/torusartist.py new file mode 100644 index 000000000000..1979e025116f --- /dev/null +++ b/src/compas_blender/artists/torusartist.py @@ -0,0 +1,55 @@ +from typing import Optional +from typing import Any +from typing import Union + +import bpy + +import compas_blender +from compas.geometry import Torus +from compas.artists import ShapeArtist +from .artist import BlenderArtist + + +class TorusArtist(BlenderArtist, ShapeArtist): + """Artist for drawing torus shapes. + + Parameters + ---------- + torus : :class:`compas.geometry.Torus` + A COMPAS torus. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. + """ + + def __init__(self, + torus: Torus, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + + super().__init__(shape=torus, collection=collection or torus.name, **kwargs) + + def draw(self, color=None, u=None, v=None): + """Draw the torus associated with the artist. + + Parameters + ---------- + color : tuple of float, optional + The RGB color of the torus. + u : int, optional + Number of faces in the "u" direction. + Default is ``~TorusArtist.u``. + v : int, optional + Number of faces in the "v" direction. + Default is ``~TorusArtist.v``. + + Returns + ------- + list + The objects created in Blender. + """ + u = u or self.u + v = v or self.v + color = color or self.color + vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection) + return [obj] diff --git a/src/compas_blender/artists/volmeshartist.py b/src/compas_blender/artists/volmeshartist.py index c1df1da783bb..d1170784446a 100644 --- a/src/compas_blender/artists/volmeshartist.py +++ b/src/compas_blender/artists/volmeshartist.py @@ -1,21 +1,17 @@ -from compas_blender.artists.meshartist import MeshArtist +from typing import Optional +from typing import Union +from typing import Any +import bpy -__all__ = [ - 'VolMeshArtist', -] +from compas.artists import MeshArtist +from .artist import BlenderArtist -class VolMeshArtist(MeshArtist): +class VolMeshArtist(BlenderArtist, MeshArtist): - def __init__(self, volmesh): - super().__init__() - self.volmesh = volmesh + def __init__(self, volmesh, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): - @property - def volmesh(self): - return self.datastructure - - @volmesh.setter - def volmesh(self, volmesh): - self.datastructure = volmesh + super().__init__(volmesh=volmesh, collection=collection or volmesh.name, **kwargs) diff --git a/src/compas_blender/utilities/collections.py b/src/compas_blender/utilities/collections.py index 1abf2eccd933..b88bb7585d10 100644 --- a/src/compas_blender/utilities/collections.py +++ b/src/compas_blender/utilities/collections.py @@ -13,6 +13,14 @@ ] +def collection_path(collection, names=[]): + for parent in bpy.data.collections: + if collection.name in parent.children: + names.append(parent.name) + collection_path(parent, names) + return names + + def create_collection(name: Text, parent: bpy.types.Collection = None) -> bpy.types.Collection: """Create a collection with the given name. @@ -28,13 +36,26 @@ def create_collection(name: Text, parent: bpy.types.Collection = None) -> bpy.ty """ if not name: return - collection = bpy.data.collections.get(name) or bpy.data.collections.new(name) + if not parent: - if collection.name not in bpy.context.scene.collection.children: - bpy.context.scene.collection.children.link(collection) + + if name in bpy.data.collections: + count = 1 + newname = f'{name}.{count:04}' + while newname in bpy.data.collections: + count += 1 + newname = f'{name}.{count:04}' + name = newname + collection = bpy.data.collections.new(name) + bpy.context.scene.collection.children.link(collection) else: - if collection.name not in parent.children: + path = collection_path(parent)[::-1] + [parent.name] + name = "::".join(path) + "::" + name + if name not in parent.children: + collection = bpy.data.collections.new(name) parent.children.link(collection) + else: + collection = bpy.data.collections.get(name) return collection diff --git a/src/compas_blender/utilities/drawing.py b/src/compas_blender/utilities/drawing.py index 44e2fcdc6be3..9ae6236ccfcb 100644 --- a/src/compas_blender/utilities/drawing.py +++ b/src/compas_blender/utilities/drawing.py @@ -316,27 +316,6 @@ def draw_spheres(spheres: List[Dict], return objects -# def draw_spheres(spheres, collection): -# add_sphere = compas_blender.bpy.ops.mesh.primitive_uv_sphere_add -# objects = [] -# for sphere in spheres: -# add_sphere(location=[0, 0, 0], radius=1.0, segments=10, ring_count=10) -# pos = sphere['pos'] -# radius = sphere['radius'] -# name = sphere['name'] -# color = sphere['color'] -# obj = compas_blender.bpy.context.active_object -# obj.location = pos -# obj.scale = radius -# obj.name = name -# compas_blender.drawing.set_object_color(obj, color) -# objects.apend(obj) -# for o in objects_vertices: -# for c in o.user_collection: -# c.objects.unlink(o) -# collection.objects.link(o) - - def draw_cubes(cubes: List[Dict], collection: Union[Text, bpy.types.Collection] = None) -> List[bpy.types.Object]: """Draw cube objects as mesh primitives.""" diff --git a/src/compas_ghpython/__init__.py b/src/compas_ghpython/__init__.py index 6b388e49bb48..d1fa20e42209 100644 --- a/src/compas_ghpython/__init__.py +++ b/src/compas_ghpython/__init__.py @@ -69,5 +69,9 @@ def _get_grasshopper_special_folder(version, folder_name): return grasshopper_library_path -__all_plugins__ = ['compas_ghpython.install', 'compas_ghpython.uninstall'] +__all_plugins__ = [ + 'compas_ghpython.install', + 'compas_ghpython.uninstall', + 'compas_ghpython.artists', +] __all__ = [name for name in dir() if not name.startswith('_')] diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index d71bafb9d12f..170b3f2d7c95 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -5,19 +5,9 @@ .. currentmodule:: compas_ghpython.artists -.. rst-class:: lead -Artists for visualising (painting) COMPAS objects with GHPython. -Artists convert COMPAS objects to Rhino geometry and data. - -.. code-block:: python - - pass - ----- - -Geometry Artists -================ +Primitive Artists +================= .. autosummary:: :toctree: generated/ @@ -59,31 +49,110 @@ :toctree: generated/ :nosignatures: - BaseArtist - PrimitiveArtist - ShapeArtist + GHArtist """ from __future__ import absolute_import -from ._artist import BaseArtist -from ._primitiveartist import PrimitiveArtist -from ._shapeartist import ShapeArtist +import inspect + +from compas.plugins import plugin +from compas.artists import Artist +from compas.artists import ShapeArtist +from compas.artists import DataArtistNotRegistered + +from compas.geometry import Circle +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Point +from compas.geometry import Polyline + +from compas.datastructures import Mesh +from compas.datastructures import Network +from compas.datastructures import VolMesh +from compas.robots import RobotModel + +from .artist import GHArtist from .circleartist import CircleArtist from .frameartist import FrameArtist from .lineartist import LineArtist from .pointartist import PointArtist from .polylineartist import PolylineArtist - from .meshartist import MeshArtist from .networkartist import NetworkArtist from .volmeshartist import VolMeshArtist - from .robotmodelartist import RobotModelArtist +ShapeArtist.default_color = (255, 255, 255) + +MeshArtist.default_color = (0, 0, 0) +MeshArtist.default_vertexcolor = (255, 255, 255) +MeshArtist.default_edgecolor = (0, 0, 0) +MeshArtist.default_facecolor = (255, 255, 255) + +NetworkArtist.default_nodecolor = (255, 255, 255) +NetworkArtist.default_edgecolor = (0, 0, 0) + +VolMeshArtist.default_color = (0, 0, 0) +VolMeshArtist.default_vertexcolor = (255, 255, 255) +VolMeshArtist.default_edgecolor = (0, 0, 0) +VolMeshArtist.default_facecolor = (255, 255, 255) +VolMeshArtist.default_cellcolor = (255, 0, 0) + + +def verify_gh_context(): + try: + import Rhino + import scriptcontext as sc + + return not isinstance(sc.doc, Rhino.RhinoDoc) + except: # noqa: E722 + return False + + +artists_registered = False + + +@plugin(category='factories', pluggable_name='new_artist', requires=['ghpythonlib', verify_gh_context]) +def new_artist_gh(cls, *args, **kwargs): + # "lazy registration" seems necessary to avoid item-artist pairs to be overwritten unintentionally + global artists_registered + + if not artists_registered: + GHArtist.register(Circle, CircleArtist) + GHArtist.register(Frame, FrameArtist) + GHArtist.register(Line, LineArtist) + GHArtist.register(Point, PointArtist) + GHArtist.register(Polyline, PolylineArtist) + GHArtist.register(Mesh, MeshArtist) + GHArtist.register(Network, NetworkArtist) + GHArtist.register(VolMesh, VolMeshArtist) + GHArtist.register(RobotModel, RobotModelArtist) + artists_registered = True + + data = args[0] + + if 'artist_type' in kwargs: + cls = kwargs['artist_type'] + else: + dtype = type(data) + if dtype not in GHArtist.ITEM_ARTIST: + raise DataArtistNotRegistered('No GH artist is registered for this data type: {}'.format(dtype)) + cls = GHArtist.ITEM_ARTIST[dtype] + + # TODO: move this to the plugin module and/or to a dedicated function + + for name, value in inspect.getmembers(cls): + if inspect.ismethod(value): + if hasattr(value, '__isabstractmethod__'): + raise Exception('Abstract method not implemented: {}'.format(value)) + + return super(Artist, cls).__new__(cls) + + __all__ = [ - 'BaseArtist', + 'GHArtist', 'PrimitiveArtist', 'ShapeArtist', 'CircleArtist', diff --git a/src/compas_ghpython/artists/_artist.py b/src/compas_ghpython/artists/_artist.py deleted file mode 100644 index efd6f48e2c05..000000000000 --- a/src/compas_ghpython/artists/_artist.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - - -__all__ = ["BaseArtist"] - - -class BaseArtist(object): - """Abstract base class for all GH artists. - """ - - def __init__(self): - pass - - def draw(self): - raise NotImplementedError diff --git a/src/compas_ghpython/artists/_primitiveartist.py b/src/compas_ghpython/artists/_primitiveartist.py deleted file mode 100644 index 5fb4dc9260f4..000000000000 --- a/src/compas_ghpython/artists/_primitiveartist.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas_ghpython.artists._artist import BaseArtist - - -__all__ = ["PrimitiveArtist"] - - -class PrimitiveArtist(BaseArtist): - """Base class for artists for geometry primitives. - - Parameters - ---------- - primitive: :class:`compas.geometry.Primitive` - The instance of the primitive. - color : 3-tuple, optional - The RGB color specification of the object. - - Attributes - ---------- - primitive: :class:`compas.geometry.Primitive` - A reference to the geometry of the primitive. - name : str - The name of the primitive. - color : tuple - The RGB components of the base color of the primitive. - - """ - - def __init__(self, primitive, color=None): - super(PrimitiveArtist, self).__init__() - self.primitive = primitive - self.color = color - - @property - def name(self): - """str : Reference to the name of the primitive.""" - return self.primitive.name - - @name.setter - def name(self, name): - self.primitive.name = name diff --git a/src/compas_ghpython/artists/_shapeartist.py b/src/compas_ghpython/artists/_shapeartist.py deleted file mode 100644 index 7656a46ee205..000000000000 --- a/src/compas_ghpython/artists/_shapeartist.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.datastructures import Mesh -from compas_ghpython.artists._artist import BaseArtist - - -__all__ = ['ShapeArtist'] - - -class ShapeArtist(BaseArtist): - """Base class for artists for geometric shapes. - - Parameters - ---------- - shape: :class:`compas.geometry.Shape` - The instance of the shape. - color : 3-tuple, optional - The RGB components of the base color of the shape. - - Attributes - ---------- - shape: :class:`compas.geometry.Shape` - A reference to the geometry of the shape. - name : str - The name of the shape. - color : tuple - The RGB components of the base color of the shape. - - """ - - def __init__(self, shape, color=None): - super(ShapeArtist, self).__init__() - self._shape = None - self._mesh = None - self.shape = shape - self.color = color - - @property - def shape(self): - return self._shape - - @shape.setter - def shape(self, shape): - self._shape = shape - self._mesh = Mesh.from_shape(shape) - - @property - def name(self): - """str : Reference to the name of the shape.""" - return self.shape.name - - @name.setter - def name(self, name): - self.shape.name = name diff --git a/src/compas_ghpython/artists/artist.py b/src/compas_ghpython/artists/artist.py new file mode 100644 index 000000000000..fd6f351fc3bb --- /dev/null +++ b/src/compas_ghpython/artists/artist.py @@ -0,0 +1,13 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.artists import Artist + + +class GHArtist(Artist): + """Base class for all GH artists. + """ + + def __init__(self, **kwargs): + super(GHArtist, self).__init__(**kwargs) diff --git a/src/compas_ghpython/artists/circleartist.py b/src/compas_ghpython/artists/circleartist.py index 7f9531588586..b45477b54d3b 100644 --- a/src/compas_ghpython/artists/circleartist.py +++ b/src/compas_ghpython/artists/circleartist.py @@ -3,40 +3,35 @@ from __future__ import division import compas_ghpython -from compas_ghpython.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import GHArtist -__all__ = ['CircleArtist'] - - -class CircleArtist(PrimitiveArtist): +class CircleArtist(GHArtist, PrimitiveArtist): """Artist for drawing circles. Parameters ---------- - primitive : :class:`compas.geometry.Circle` + circle : :class:`compas.geometry.Circle` A COMPAS circle. - - Other Parameters - ---------------- - See :class:`compas_ghpython.artists.PrimitiveArtist` for all other parameters. - """ + def __init__(self, circle, **kwargs): + super(CircleArtist, self).__init__(primitive=circle, **kwargs) + def draw(self): """Draw the circle. Returns ------- :class:`Rhino.Geometry.Circle` - """ - circles = [self._get_args(self.primitive)] + circles = [self._get_args(self.primitive, self.color)] return compas_ghpython.draw_circles(circles)[0] @staticmethod - def _get_args(primitive): + def _get_args(primitive, color=None): point = list(primitive.plane.point) normal = list(primitive.plane.normal) radius = primitive.radius - return {'plane': [point, normal], 'radius': radius, 'color': None, 'name': primitive.name} + return {'plane': [point, normal], 'radius': radius, 'color': color, 'name': primitive.name} diff --git a/src/compas_ghpython/artists/frameartist.py b/src/compas_ghpython/artists/frameartist.py index f0d63f3cdc4c..3df4bb69a839 100644 --- a/src/compas_ghpython/artists/frameartist.py +++ b/src/compas_ghpython/artists/frameartist.py @@ -3,21 +3,17 @@ from __future__ import division import compas_ghpython -from compas_ghpython.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import GHArtist -__all__ = ['FrameArtist'] - - -class FrameArtist(PrimitiveArtist): +class FrameArtist(GHArtist, PrimitiveArtist): """Artist for drawing frames. Parameters ---------- - frame : compas.geometry.Frame + frame : :class:`compas.geometry.Frame` A COMPAS frame. - name : str, optional - The name of the frame. scale : float, optional The scale of the vectors representing the axes of the frame. Default is ``1.0``. @@ -25,19 +21,20 @@ class FrameArtist(PrimitiveArtist): Attributes ---------- scale : float - color_origin : tuple of 3 int between 0 abd 255 - color_xaxis : tuple of 3 int between 0 abd 255 - color_yaxis : tuple of 3 int between 0 abd 255 - color_zaxis : tuple of 3 int between 0 abd 255 - - Examples - -------- - >>> - + Scale factor that controls the length of the axes. + Default is ``1.0``. + color_origin : tuple of 3 int between 0 and 255 + Default is ``(0, 0, 0)``. + color_xaxis : tuple of 3 int between 0 and 255 + Default is ``(255, 0, 0)``. + color_yaxis : tuple of 3 int between 0 and 255 + Default is ``(0, 255, 0)``. + color_zaxis : tuple of 3 int between 0 and 255 + Default is ``(0, 0, 255)``. """ - def __init__(self, frame, scale=1.0): - super(FrameArtist, self).__init__(frame) + def __init__(self, frame, scale=1.0, **kwargs): + super(FrameArtist, self).__init__(primitive=frame, **kwargs) self.scale = scale self.color_origin = (0, 0, 0) self.color_xaxis = (255, 0, 0) @@ -50,7 +47,6 @@ def draw(self): Returns ------- :class:`Rhino.Geometry.Plane` - """ return compas_ghpython.draw_frame(self.primitive) @@ -60,7 +56,6 @@ def draw_origin(self): Returns ------- :class:`Rhino.Geometry.Point` - """ point, _ = self._get_args(self.primitive, self.scale, self.color_origin, self.color_xaxis, self.color_yaxis, self.color_zaxis) return compas_ghpython.draw_points([point])[0] @@ -71,7 +66,6 @@ def draw_axes(self): Returns ------- list of :class:`Rhino.Geometry.Line` - """ _, lines = self._get_args(self.primitive, self.scale, self.color_origin, self.color_xaxis, self.color_yaxis, self.color_zaxis) return compas_ghpython.draw_lines(lines) diff --git a/src/compas_ghpython/artists/lineartist.py b/src/compas_ghpython/artists/lineartist.py index 148376737298..3579ed54a1fd 100644 --- a/src/compas_ghpython/artists/lineartist.py +++ b/src/compas_ghpython/artists/lineartist.py @@ -3,33 +3,28 @@ from __future__ import division import compas_ghpython -from compas_ghpython.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import GHArtist -__all__ = ['LineArtist'] - - -class LineArtist(PrimitiveArtist): +class LineArtist(GHArtist, PrimitiveArtist): """Artist for drawing lines. Parameters ---------- - primitive : :class:`compas.geometry.Line` + line : :class:`compas.geometry.Line` A COMPAS line. - - Other Parameters - ---------------- - See :class:`compas_ghpython.artists.PrimitiveArtist` for all other parameters. - """ + def __init__(self, line, **kwargs): + super(LineArtist, self).__init__(primitive=line, **kwargs) + def draw(self): """Draw the line. Returns ------- :class:`Rhino.Geometry.Line` - """ lines = [self._get_args(self.primitive)] return compas_ghpython.draw_lines(lines)[0] diff --git a/src/compas_ghpython/artists/meshartist.py b/src/compas_ghpython/artists/meshartist.py index 1e62cd94fc11..ae8a1dc23a4d 100644 --- a/src/compas_ghpython/artists/meshartist.py +++ b/src/compas_ghpython/artists/meshartist.py @@ -2,91 +2,54 @@ from __future__ import division from __future__ import print_function -from functools import partial - import Rhino - -import compas_ghpython -from compas_ghpython.artists._artist import BaseArtist +from functools import partial from compas.geometry import centroid_polygon from compas.utilities import color_to_colordict from compas.utilities import pairwise +import compas_ghpython +from compas.artists import MeshArtist +from .artist import GHArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['MeshArtist'] - - -class MeshArtist(BaseArtist): - """A mesh artist defines functionality for visualising COMPAS meshes in GhPython. +class MeshArtist(GHArtist, MeshArtist): + """Artist for drawing mesh data structures. Parameters ---------- mesh : :class:`compas.datastructures.Mesh` A COMPAS mesh. - - Attributes - ---------- - mesh : :class:`compas.datastructures.Mesh` - The COMPAS mesh associated with the artist. - color_vertices : 3-tuple - Default color of the vertices. - color_edges : 3-tuple - Default color of the edges. - color_faces : 3-tuple - Default color of the faces. - - Examples - -------- - .. code-block:: python - - import compas - from compas.datastructures import Mesh - from compas_ghpython.artists import MeshArtist - - mesh = Mesh.from_obj(compas.get('faces.obj')) - - artist = MeshArtist(mesh) - artist.draw_faces(join_faces=True) - artist.draw_vertices(color={key: '#ff0000' for key in mesh.vertices_on_boundary()}) - artist.draw_edges() - """ - def __init__(self, mesh): - self._mesh = None - self.mesh = mesh - self.color_vertices = (255, 255, 255) - self.color_edges = (0, 0, 0) - self.color_faces = (210, 210, 210) - - @property - def mesh(self): - """compas.datastructures.Mesh: The mesh that should be painted.""" - return self._mesh - - @mesh.setter - def mesh(self, mesh): - self._mesh = mesh + def __init__(self, mesh, **kwargs): + super(MeshArtist, self).__init__(mesh=mesh, **kwargs) def draw(self, color=None): """Draw the mesh as a RhinoMesh. Parameters ---------- - color : 3-tuple, optional - RGB color components in integer format (0-255). + color : tuple, optional + The color of the mesh. + Default is the value of ``~MeshArtist.default_color``. Returns ------- :class:`Rhino.Geometry.Mesh` + Notes + ----- + The mesh should be a valid Rhino Mesh object, which means it should have only triangular or quadrilateral faces. + Faces with more than 4 vertices will be triangulated on-the-fly. """ - vertex_index = self.mesh.key_index() - vertices = self.mesh.vertices_attributes('xyz') + color = color or self.default_color + vertex_index = self.mesh.vertex_index() + vertex_xyz = self.vertex_xyz + vertices = [vertex_xyz[vertex] for vertex in self.mesh.vertices()] faces = [[vertex_index[vertex] for vertex in self.mesh.face_vertices(face)] for face in self.mesh.faces()] new_faces = [] for face in faces: @@ -101,8 +64,6 @@ def draw(self, color=None): [vertices[index] for index in face])) for a, b in pairwise(face + face[0:1]): new_faces.append([centroid, a, b, b]) - else: - continue return compas_ghpython.draw_mesh(vertices, new_faces, color) def draw_vertices(self, vertices=None, color=None): @@ -113,23 +74,25 @@ def draw_vertices(self, vertices=None, color=None): vertices : list, optional A selection of vertices to draw. Default is ``None``, in which case all vertices are drawn. - color : 3-tuple or dict of 3-tuple, optional + color : tuple or dict of tuple, optional The color specififcation for the vertices. - The default color is ``(255, 255, 255)``. + The default is the value of ``~MeshArtist.default_vertexcolor``. Returns ------- list of :class:`Rhino.Geometry.Point3d` """ + self.vertex_color = color vertices = vertices or list(self.mesh.vertices()) - vertex_color = colordict(color, vertices, default=self.color_vertices) + vertex_xyz = self.vertex_xyz points = [] for vertex in vertices: points.append({ - 'pos': self.mesh.vertex_coordinates(vertex), + 'pos': vertex_xyz[vertex], 'name': "{}.vertex.{}".format(self.mesh.name, vertex), - 'color': vertex_color[vertex]}) + 'color': self.vertex_color.get(vertex, self.default_vertexcolor) + }) return compas_ghpython.draw_points(points) def draw_faces(self, faces=None, color=None, join_faces=False): @@ -137,27 +100,32 @@ def draw_faces(self, faces=None, color=None, join_faces=False): Parameters ---------- - faces : list + faces : list, optional A selection of faces to draw. The default is ``None``, in which case all faces are drawn. - color : 3-tuple or dict of 3-tuple, optional + color : tuple or dict of tuple, optional The color specififcation for the faces. - The default color is ``(0, 0, 0)``. + The default color is the value of ``~MeshArtist.default_facecolor``. + join_faces : bool, optional + Join the faces into 1 mesh. + Default is ``False``, in which case the faces are drawn as individual meshes. Returns ------- list of :class:`Rhino.Geometry.Mesh` """ + self.face_color = color faces = faces or list(self.mesh.faces()) - face_color = colordict(color, faces, default=self.color_faces) - faces_ = [] + vertex_xyz = self.vertex_xyz + facets = [] for face in faces: - faces_.append({ - 'points': self.mesh.face_coordinates(face), + facets.append({ + 'points': [vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)], 'name': "{}.face.{}".format(self.mesh.name, face), - 'color': face_color[face]}) - meshes = compas_ghpython.draw_faces(faces_) + 'color': self.face_color.get(face, self.default_facecolor) + }) + meshes = compas_ghpython.draw_faces(facets) if not join_faces: return meshes joined_mesh = Rhino.Geometry.Mesh() @@ -173,23 +141,36 @@ def draw_edges(self, edges=None, color=None): edges : list, optional A selection of edges to draw. The default is ``None``, in which case all edges are drawn. - color : 3-tuple or dict of 3-tuple, optional + color : tuple or dict of tuple, optional The color specififcation for the edges. - The default color is ``(210, 210, 210)``. + The default color is the value of ``~MeshArtist.default_edgecolor``. Returns ------- list of :class:`Rhino.Geometry.Line` """ + self.edge_color = color edges = edges or list(self.mesh.edges()) - edge_color = colordict(color, edges, default=self.color_edges) + vertex_xyz = self.vertex_xyz lines = [] for edge in edges: - start, end = self.mesh.edge_coordinates(*edge) lines.append({ - 'start': start, - 'end': end, - 'color': edge_color[edge], - 'name': "{}.edge.{}-{}".format(self.mesh.name, *edge)}) + 'start': vertex_xyz[edge[0]], + 'end': vertex_xyz[edge[1]], + 'color': self.edge_color.get(edge, self.default_edgecolor), + 'name': "{}.edge.{}-{}".format(self.mesh.name, *edge) + }) return compas_ghpython.draw_lines(lines) + + def clear_edges(self): + """GH Artists are state-less. Clear does not have any effect.""" + pass + + def clear_vertices(self): + """GH Artists are state-less. Clear does not have any effect.""" + pass + + def clear_faces(self): + """GH Artists are state-less. Clear does not have any effect.""" + pass diff --git a/src/compas_ghpython/artists/networkartist.py b/src/compas_ghpython/artists/networkartist.py index f8db95471144..9d9a4979dd94 100644 --- a/src/compas_ghpython/artists/networkartist.py +++ b/src/compas_ghpython/artists/networkartist.py @@ -4,49 +4,26 @@ from functools import partial import compas_ghpython -from compas_ghpython.artists._artist import BaseArtist + from compas.utilities import color_to_colordict +from compas.artists import NetworkArtist +from .artist import GHArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['NetworkArtist'] - - -class NetworkArtist(BaseArtist): - """A network artist defines functionality for visualising COMPAS networks in GhPython. +class NetworkArtist(GHArtist, NetworkArtist): + """Artist for drawing network data structures. Parameters ---------- - network : compas.datastructures.Network - A COMPAS network. - - Attributes - ---------- network : :class:`compas.datastructures.Network` - The COMPAS network associated with the artist. - color_nodes : 3-tuple - Default color of the nodes. - color_edges : 3-tuple - Default color of the edges. - + A COMPAS network. """ - def __init__(self, network): - self._network = None - self.network = network - self.color_nodes = (255, 255, 255) - self.color_edges = (0, 0, 0) - - @property - def network(self): - """compas.datastructures.Network: The network that should be painted.""" - return self._network - - @network.setter - def network(self, network): - self._network = network + def __init__(self, network, **kwargs): + super(NetworkArtist, self).__init__(network=network, **kwargs) def draw(self): """Draw the entire network with default color settings. @@ -58,10 +35,6 @@ def draw(self): """ return (self.draw_nodes(), self.draw_edges()) - # ============================================================================== - # components - # ============================================================================== - def draw_nodes(self, nodes=None, color=None): """Draw a selection of nodes. @@ -77,16 +50,17 @@ def draw_nodes(self, nodes=None, color=None): Returns ------- list of :class:`Rhino.Geometry.Point3d` - """ + self.node_color = color + node_xyz = self.node_xyz nodes = nodes or list(self.network.nodes()) - node_color = colordict(color, nodes, default=self.color_nodes) points = [] for node in nodes: points.append({ - 'pos': self.network.node_coordinates(node), + 'pos': node_xyz[node], 'name': "{}.node.{}".format(self.network.name, node), - 'color': node_color[node]}) + 'color': self.node_color.get(node, self.default_nodecolor) + }) return compas_ghpython.draw_points(points) def draw_edges(self, edges=None, color=None): @@ -97,23 +71,23 @@ def draw_edges(self, edges=None, color=None): edges : list, optional A list of edges to draw. The default is ``None``, in which case all edges are drawn. - color : 3-tuple or dict of 3-tuple, optional + color : tuple or dict of tuple, optional The color specififcation for the edges. - The default color is ``(0, 0, 0)``. + The default color is the value of ``~NetworkArtist.default_edgecolor``. Returns ------- list of :class:`Rhino.Geometry.Line` - """ + self.edge_color = color + node_xyz = self.node_xyz edges = edges or list(self.network.edges()) - edge_color = colordict(color, edges, default=self.color_edges) lines = [] for edge in edges: - start, end = self.network.edge_coordinates(*edge) lines.append({ - 'start': start, - 'end': end, - 'color': edge_color[edge], - 'name': "{}.edge.{}-{}".format(self.network.name, *edge)}) + 'start': node_xyz[edge[0]], + 'end': node_xyz[edge[1]], + 'color': self.edge_color.get(edge, self.default_edgecolor), + 'name': "{}.edge.{}-{}".format(self.network.name, *edge) + }) return compas_ghpython.draw_lines(lines) diff --git a/src/compas_ghpython/artists/pointartist.py b/src/compas_ghpython/artists/pointartist.py index dd3e0c6e5c35..68408fe7a5c1 100644 --- a/src/compas_ghpython/artists/pointartist.py +++ b/src/compas_ghpython/artists/pointartist.py @@ -3,33 +3,28 @@ from __future__ import division import compas_ghpython -from compas_ghpython.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import GHArtist -__all__ = ['PointArtist'] - - -class PointArtist(PrimitiveArtist): +class PointArtist(GHArtist, PrimitiveArtist): """Artist for drawing points. Parameters ---------- - primitive : :class:`compas.geometry.Point` + point : :class:`compas.geometry.Point` A COMPAS point. - - Other Parameters - ---------------- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - """ + def __init__(self, point, **kwargs): + super(PointArtist, self).__init__(primitive=point, **kwargs) + def draw(self): """Draw the point. Returns ------- :class:`Rhino.Geometry.Point3d` - """ points = [self._get_args(self.primitive)] return compas_ghpython.draw_points(points)[0] diff --git a/src/compas_ghpython/artists/polylineartist.py b/src/compas_ghpython/artists/polylineartist.py index 12496d90664e..8b545fd7f950 100644 --- a/src/compas_ghpython/artists/polylineartist.py +++ b/src/compas_ghpython/artists/polylineartist.py @@ -3,26 +3,23 @@ from __future__ import division import compas_ghpython -from compas_ghpython.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import GHArtist -__all__ = ['PolylineArtist'] - - -class PolylineArtist(PrimitiveArtist): +class PolylineArtist(GHArtist, PrimitiveArtist): """Artist for drawing polylines. Parameters ---------- - primitive : :class:`compas.geometry.Polyline` + polyline : :class:`compas.geometry.Polyline` A COMPAS polyline. - Other Parameters - ---------------- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - """ + def __init__(self, polyline, **kwargs): + super(PolylineArtist, self).__init__(primitive=polyline, **kwargs) + def draw(self): """Draw the polyline. diff --git a/src/compas_ghpython/artists/robotmodelartist.py b/src/compas_ghpython/artists/robotmodelartist.py index 9fc3e218c2ee..fdc1e0690932 100644 --- a/src/compas_ghpython/artists/robotmodelartist.py +++ b/src/compas_ghpython/artists/robotmodelartist.py @@ -2,21 +2,17 @@ from __future__ import division from __future__ import print_function -from compas.robots.base_artist import BaseRobotModelArtist from compas.utilities import rgb_to_rgb -from compas_ghpython.utilities import draw_mesh -from compas_ghpython.artists import BaseArtist from compas_rhino.geometry.transformations import xtransform - -__all__ = [ - 'RobotModelArtist', -] +from compas.artists import RobotModelArtist +from compas_ghpython.utilities import draw_mesh +from .artist import GHArtist -class RobotModelArtist(BaseRobotModelArtist, BaseArtist): - """Visualizer for robots inside a Grasshopper environment. +class RobotModelArtist(GHArtist, RobotModelArtist): + """Artist for drawing robot models. Parameters ---------- @@ -24,8 +20,8 @@ class RobotModelArtist(BaseRobotModelArtist, BaseArtist): Robot model. """ - def __init__(self, model): - super(RobotModelArtist, self).__init__(model) + def __init__(self, model, **kwargs): + super(RobotModelArtist, self).__init__(model=model, **kwargs) def transform(self, native_mesh, transformation): xtransform(native_mesh, transformation) @@ -34,13 +30,10 @@ def create_geometry(self, geometry, name=None, color=None): if color: color = rgb_to_rgb(color[0], color[1], color[2]) vertices, faces = geometry.to_vertices_and_faces() - mesh = draw_mesh(vertices, faces, color=color) - # Try to fix invalid meshes if not mesh.IsValid: mesh.FillHoles() - return mesh def draw(self): diff --git a/src/compas_ghpython/artists/volmeshartist.py b/src/compas_ghpython/artists/volmeshartist.py index 7c38c9890fd9..d2587f20be1a 100644 --- a/src/compas_ghpython/artists/volmeshartist.py +++ b/src/compas_ghpython/artists/volmeshartist.py @@ -6,56 +6,26 @@ import Rhino -import compas_ghpython -from compas_ghpython.artists._artist import BaseArtist - -# from compas.geometry import centroid_polygon from compas.utilities import color_to_colordict -# from compas.utilities import pairwise +import compas_ghpython +from compas.artists import VolMeshArtist +from .artist import GHArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['VolMeshArtist'] - - -class VolMeshArtist(BaseArtist): - """A volmesh artist defines functionality for visualising COMPAS volmeshes in GhPython. +class VolMeshArtist(GHArtist, VolMeshArtist): + """Artist for drawing volmesh data structures. Parameters ---------- volmesh : :class:`compas.datastructures.VolMesh` A COMPAS volmesh. - - Attributes - ---------- - volmesh : :class:`compas.datastructures.VolMesh` - The COMPAS volmesh associated with the artist. - color_vertices : 3-tuple - Default color of the vertices. - color_edges : 3-tuple - Default color of the edges. - color_faces : 3-tuple - Default color of the faces. - """ - def __init__(self, volmesh): - self._volmesh = None - self.volmesh = volmesh - self.color_vertices = (255, 255, 255) - self.color_edges = (0, 0, 0) - self.color_faces = (210, 210, 210) - - @property - def volmesh(self): - """compas.datastructures.VolMesh: The volmesh that should be painted.""" - return self._volmesh - - @volmesh.setter - def volmesh(self, volmesh): - self._volmesh = volmesh + def __init__(self, volmesh, **kwargs): + super(VolMeshArtist, self).__init__(volmesh=volmesh, **kwargs) def draw(self): """""" @@ -66,86 +36,88 @@ def draw_vertices(self, vertices=None, color=None): Parameters ---------- - vertices : list, optional - A selection of vertices to draw. + vertices : list + A list of vertices to draw. Default is ``None``, in which case all vertices are drawn. - color : 3-tuple or dict of 3-tuple, optional + color : str, tuple, dict The color specififcation for the vertices. - The default color is ``(255, 255, 255)``. + The default color of the vertices is ``~VolMeshArtist.default_vertexcolor``. Returns ------- list of :class:`Rhino.Geometry.Point3d` - """ + self.vertex_color = color vertices = vertices or list(self.volmesh.vertices()) - vertex_color = colordict(color, vertices, default=self.color_vertices) + vertex_xyz = self.vertex_xyz points = [] for vertex in vertices: points.append({ - 'pos': self.volmesh.vertex_coordinates(vertex), + 'pos': vertex_xyz[vertex], 'name': "{}.vertex.{}".format(self.volmesh.name, vertex), - 'color': vertex_color[vertex]}) + 'color': self.vertex_color.get(vertex, self.default_vertexcolor) + }) return compas_ghpython.draw_points(points) + def draw_edges(self, edges=None, color=None): + """Draw a selection of edges. + + Parameters + ---------- + edges : list, optional + A list of edges to draw. + The default is ``None``, in which case all edges are drawn. + color : str, tuple, dict + The color specififcation for the edges. + The default color is ``~VolMeshArtist.default_edgecolor``. + + Returns + ------- + list of :class:`Rhino.Geometry.Line` + """ + self.edge_color = color + edges = edges or list(self.volmesh.edges()) + vertex_xyz = self.vertex_xyz + lines = [] + for edge in edges: + lines.append({ + 'start': vertex_xyz[edge[0]], + 'end': vertex_xyz[edge[1]], + 'color': self.edge_color.get(edge, self.default_edgecolor), + 'name': "{}.edge.{}-{}".format(self.volmesh.name, *edge) + }) + return compas_ghpython.draw_lines(lines) + def draw_faces(self, faces=None, color=None, join_faces=False): """Draw a selection of faces. Parameters ---------- - faces : list - A selection of faces to draw. + faces : list, optional + A list of faces to draw. The default is ``None``, in which case all faces are drawn. - color : 3-tuple or dict of 3-tuple, optional + color : str, tuple, dict The color specififcation for the faces. - The default color is ``(210, 210, 210)``. + The default color is ``~VolMeshArtist.default_facecolor``. Returns ------- list of :class:`Rhino.Geometry.Mesh` - """ + self.face_color = color faces = faces or list(self.volmesh.faces()) - face_color = colordict(color, faces, default=self.color_faces) - faces_ = [] + vertex_xyz = self.vertex_xyz + facets = [] for face in faces: - faces_.append({ - 'points': self.volmesh.face_coordinates(face), + facets.append({ + 'points': [vertex_xyz[vertex] for vertex in self.volmesh.halfface_vertices(face)], 'name': "{}.face.{}".format(self.volmesh.name, face), - 'color': face_color[face]}) - meshes = compas_ghpython.draw_faces(faces_) + 'color': self.face_color.get(face, self.default_facecolor) + }) + meshes = compas_ghpython.draw_faces(facets) if not join_faces: return meshes joined_mesh = Rhino.Geometry.Mesh() for mesh in meshes: joined_mesh.Append(mesh) return [joined_mesh] - - def draw_edges(self, edges=None, color=None): - """Draw a selection of edges. - - Parameters - ---------- - edges : list, optional - A selection of edges to draw. - The default is ``None``, in which case all edges are drawn. - color : 3-tuple or dict of 3-tuple, optional - The color specififcation for the edges. - The default color is ``(0, 0, 0)``. - - Returns - ------- - list of :class:`Rhino.Geometry.Line` - - """ - edges = edges or list(self.volmesh.edges()) - edge_color = colordict(color, edges, default=self.color_edges) - lines = [] - for edge in edges: - start, end = self.volmesh.edge_coordinates(*edge) - lines.append({ - 'start': start, - 'end': end, - 'color': edge_color[edge], - 'name': "{}.edge.{}-{}".format(self.volmesh.name, *edge)}) - return compas_ghpython.draw_lines(lines) diff --git a/src/compas_plotters/__init__.py b/src/compas_plotters/__init__.py index 19733bbfc79c..5d6930a015d2 100644 --- a/src/compas_plotters/__init__.py +++ b/src/compas_plotters/__init__.py @@ -7,43 +7,6 @@ .. currentmodule:: compas_plotters -.. code-block:: python - - import random - import compas - - from compas.geometry import Point, Line, Polygon, Polyline, Circle, Ellipse - from compas.datastructures import Mesh - from compas_plotters import Plotter - - a = Point(0, 0, 0) - b = Point(-3, 3, 0) - - mesh = Mesh.from_obj(compas.get('faces.obj')) - points = mesh.vertices_attributes('xyz') - - plotter = Plotter() - - plotter.add(a) - plotter.add(b) - plotter.add(Line(a, b)) - plotter.add(b - a) - - plotter.add(Polyline(random.sample(points, 7)), linewidth=3.0, color=(1.0, 0, 0)) - plotter.add(Polygon(random.sample(points, 7)), facecolor=(0, 0, 1.0)) - - circles = [Circle([point, [0, 0, 1]], random.random()) for point in random.sample(points, 7)] - ellipses = [Ellipse([point, [0, 0, 1]], random.random(), random.random()) for point in random.sample(points, 7)] - - plotter.add_from_list(circles, facecolor=(0, 1, 1)) - plotter.add_from_list(ellipses, facecolor=(0, 1, 0)) - - plotter.add(mesh) - - plotter.zoom_extents() - plotter.show() - - Classes ======= @@ -53,33 +16,19 @@ Plotter - -Deprecated -========== - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - GeometryPlotter - NetworkPlotter - MeshPlotter - """ __version__ = '1.8.1' from .core import * # noqa: F401 F403 -from .artists import * # noqa: F401 F403 - -from ._plotter import BasePlotter # noqa: F401 -from .networkplotter import NetworkPlotter # noqa: F401 -from .meshplotter import MeshPlotter # noqa: F401 -from .geometryplotter import GeometryPlotter # noqa: F401 - +# from .artists import * # noqa: F401 F403 from .plotter import Plotter __all__ = [ 'Plotter' ] + +__all_plugins__ = [ + 'compas_plotters.artists', +] diff --git a/src/compas_plotters/_plotter.py b/src/compas_plotters/_plotter.py deleted file mode 100644 index e45262cb341e..000000000000 --- a/src/compas_plotters/_plotter.py +++ /dev/null @@ -1,609 +0,0 @@ -import os -import shutil - -import subprocess - -from contextlib import contextmanager - -import matplotlib.pyplot as plt - -from matplotlib.patches import Circle -from matplotlib.patches import FancyArrowPatch -from matplotlib.patches import ArrowStyle - -from compas_plotters.core.drawing import create_axes_xy -from compas_plotters.core.drawing import draw_xpoints_xy -from compas_plotters.core.drawing import draw_xlines_xy -from compas_plotters.core.drawing import draw_xpolylines_xy -from compas_plotters.core.drawing import draw_xpolygons_xy -from compas_plotters.core.drawing import draw_xarrows_xy - - -__all__ = [ - 'BasePlotter', - 'valuedict' -] - - -def valuedict(keys, value, default): - """ - Build value dictionary from a list of keys and a value. - - Parameters - ---------- - keys: list - The list of keys - value: {dict, int, float, str, None} - A value or the already formed dictionary - default: {int, float, str} - A default value to set if no value - - Returns - ------- - dict - A dictionary - - Notes - ----- - This standalone and generic function is only required by plotters. - - """ - if isinstance(value, dict): - return {key: value.get(key, default) for key in keys} - else: - return dict.fromkeys(keys, value or default) - - -class BasePlotter: - """Definition of a plotter object based on matplotlib. - - Parameters - ---------- - figsize : tuple, optional - The size of the plot in inches (width, length). Default is ``(16.0, 12.0)``. - - Other Parameters - ---------------- - dpi : float, optional - The resolution of the plot in "dots per inch". - Default is ``100.0``. - tight : bool, optional - Produce a plot with limited padding between the plot and the edge of the figure. - Default is ``True``. - fontsize : int, optional - The size of the font used in labels. Default is ``10``. - axes : matplotlib.axes.Axes, optional - An instance of ``matplotlib`` ``Axes``. - For example to share the axes of a figure between different plotters. - Default is ``None`` in which case the plotter will make its own axes. - - Attributes - ---------- - defaults : dict - Dictionary containing default attributes for vertices and edges. - - Notes - ----- - For more info, see [1]_. - - References - ---------- - .. [1] Hunter, J. D., 2007. *Matplotlib: A 2D graphics environment*. - Computing In Science & Engineering (9) 3, p.90-95. - Available at: http://ieeexplore.ieee.org/document/4160265/citations. - - """ - - def __init__(self, figsize=(16.0, 12.0), dpi=100.0, tight=True, axes=None, fontsize=10, **kwargs): - """Initializes a plotter object""" - self._axes = None - self.axes = axes - self.tight = tight - # use descriptors for these - # to help the user set these attributes in the right format - # figure attributes - self.figure_size = figsize - self.figure_dpi = dpi - self.figure_bgcolor = '#ffffff' - # axes attributes - self.axes_xlabel = None - self.axes_ylabel = None - # drawing defaults - # z-order - # color - # size/thickness - self.defaults = { - 'point.radius': 0.1, - 'point.facecolor': '#ffffff', - 'point.edgecolor': '#000000', - 'point.edgewidth': 0.5, - 'point.textcolor': '#000000', - 'point.fontsize': fontsize, - - 'line.width': 1.0, - 'line.color': '#000000', - 'line.textcolor': '#000000', - 'line.fontsize': fontsize, - - 'polygon.facecolor': '#ffffff', - 'polygon.edgecolor': '#000000', - 'polygon.edgewidth': 0.1, - 'polygon.textcolor': '#000000', - 'polygon.fontsize': fontsize, - } - - @property - def axes(self): - """Returns the axes subplot matplotlib object. - - Returns - ------- - Axes - The matplotlib axes object. - - Notes - ----- - For more info, see the documentation of the Axes class ([1]_) and the - axis and tick API ([2]_). - - References - ---------- - .. [1] https://matplotlib.org/api/axes_api.html - .. [2] https://matplotlib.org/api/axis_api.html - - """ - if self._axes is None: - self._axes = create_axes_xy( - figsize=self.figure_size, - dpi=self.figure_dpi, - xlabel=self.axes_xlabel, - ylabel=self.axes_ylabel - ) - - return self._axes - - @axes.setter - def axes(self, axes): - self._axes = axes - - @property - def figure(self): - """Returns the matplotlib figure instance. - - Returns - ------- - Figure - The matplotlib figure instance. - - Notes - ----- - For more info, see the figure API ([1]_). - - References - ---------- - .. [1] https://matplotlib.org/2.0.2/api/figure_api.html - - """ - return self.axes.get_figure() - - @property - def canvas(self): - """Returns the canvas of the figure instance. - """ - return self.figure.canvas - - @property - def bgcolor(self): - """Returns the background color. - - Returns - ------- - str - The color as a string (hex colors). - - """ - return self.figure.get_facecolor() - - @bgcolor.setter - def bgcolor(self, value): - """Sets the background color. - - Parameters - ---------- - value : str, tuple - The color specification for the figure background. - Colors should be specified in the form of a string (hex colors) or - as a tuple of normalized RGB components. - - """ - self.figure.set_facecolor(value) - - @property - def title(self): - """Returns the title of the plot. - - Returns - ------- - str - The title of the plot. - - """ - return self.figure.canvas.get_window_title() - - @title.setter - def title(self, value): - """Sets the title of the plot. - - Parameters - ---------- - value : str - The title of the plot. - - """ - self.figure.canvas.set_window_title(value) - - def register_listener(self, listener): - """Register a listener for pick events. - - Parameters - ---------- - listener : callable - The handler for pick events. - - Returns - ------- - None - - Notes - ----- - For more information, see the docs of ``mpl_connect`` ([1]_), and on event - handling and picking ([2]_). - - References - ---------- - .. [1] https://matplotlib.org/api/backend_bases_api.html#matplotlib.backend_bases.FigureCanvasBase.mpl_connect - .. [2] https://matplotlib.org/users/event_handling.html - - Examples - -------- - .. code-block:: python - - # - - """ - self.figure.canvas.mpl_connect('pick_event', listener) - - def clear_collection(self, collection): - """Clears a matplotlib collection object. - - Parameters - ---------- - collection : object - The matplotlib collection object. - - Notes - ----- - For more info, see [1]_ and [2]_. - - References - ---------- - .. [1] https://matplotlib.org/2.0.2/api/collections_api.html - .. [2] https://matplotlib.org/2.0.2/api/collections_api.html#matplotlib.collections.Collection.remove - - """ - collection.remove() - - def show(self, autoscale=True): - """Displays the plot. - """ - if autoscale: - self.axes.autoscale() - if self.tight: - plt.tight_layout() - plt.show() - - def top(self): - """Bring the plotting window to the top. - - Warnings - -------- - This seems to work only for some back-ends. - - Notes - ----- - For more info, see this SO post [1]_. - - References - ---------- - .. [1] https://stackoverflow.com/questions/20025077/how-do-i-display-a-matplotlib-figure-window-on-top-of-all-other-windows-in-spyde - - """ - self.figure.canvas.manager.show() - - def save(self, filepath, **kwargs): - """Saves the plot to a file. - - Parameters - ---------- - filepath : str - Full path of the file. - - Notes - ----- - For an overview of all configuration options, see [1]_. - - References - ---------- - .. [1] https://matplotlib.org/2.0.2/api/pyplot_api.html#matplotlib.pyplot.savefig - - """ - self.axes.autoscale() - plt.savefig(filepath, **kwargs) - - @contextmanager - def gifified(self, func, tempfolder, outfile, pattern='image_{}.png'): - """Create a context for making animated gifs using a callback for updating the plot. - - Parameters - ---------- - func : callable - The callback function used to update the plot. - tempfolder : str - The path to a folder for storing temporary image frames. - outfile : str - Path to the file where the resultshould be saved. - pattern : str, optional - Pattern for the filename of the intermediate frames. - The pattern should contain a replacement placeholder for the number - of the frame. Default is ``'image_{}.png'``. - """ - images = [] - - def gifify(f): - def wrapper(*args, **kwargs): - f(*args, **kwargs) - image = os.path.join(tempfolder, pattern.format(len(images))) - images.append(image) - self.save(image) - return wrapper - - if not os.path.exists(tempfolder) or not os.path.isdir(tempfolder): - os.makedirs(tempfolder) - - for file in os.listdir(tempfolder): - filepath = os.path.join(tempfolder, file) - try: - if os.path.isfile(filepath): - os.remove(filepath) - except Exception as e: - print(e) - - image = os.path.join(tempfolder, pattern.format(len(images))) - images.append(image) - self.save(image) - # - yield gifify(func) - # - self.save_gif(outfile, images) - shutil.rmtree(tempfolder) - print('done gififying!') - - def save_gif(self, filepath, images, delay=10, loop=0): - """Save a series of images as an animated gif. - - Parameters - ---------- - filepath : str - The full path to the output file. - images : list - A list of paths to input files. - delay : int, optional - The delay between frames in milliseconds. Default is ``10``. - loop : int, optional - The number of loops. Default is ``0``. - - Returns - ------- - None - - Warnings - -------- - This function assumes ImageMagick is installed on your system, and on - *convert* being on your system path. - """ - command = ['convert', '-delay', '{}'.format(delay), '-loop', '{}'.format(loop), '-layers', 'optimize'] - subprocess.call(command + images + [filepath]) - - def draw_points(self, points): - """Draws points on a 2D plot. - - Parameters - ---------- - - points : list of dict - List of dictionaries containing the point properties. - Each point is represented by a circle with a given radius. - The following properties of the circle can be specified in the point dict. - - * pos (list): XY(Z) coordinates - * radius (float, optional): the radius of the circle. Default is 0.1. - * text (str, optional): the text of the label. Default is None. - * facecolor (rgb or hex color, optional): The color of the face of the circle. Default is white. - * edgecolor (rgb or hex color, optional): The color of the edge of the cicrle. Default is black. - * edgewidth (float, optional): The width of the edge of the circle. Default is 1.0. - * textcolor (rgb or hex color, optional): Color of the text label. Default is black. - * fontsize (int, optional): Font size of the text label. Default is ``12``. - - Returns - ------- - object - The matplotlib point collection object. - - """ - return draw_xpoints_xy(points, self.axes) - - def draw_lines(self, lines): - """Draws lines on a 2D plot. - - Parameters - ---------- - lines : list of dict - List of dictionaries containing the line properties. - The following properties of a line can be specified in the dict. - - * start (list): XY(Z) coordinates of the start point. - * end (list): XY(Z) coordinatesof the end point. - * width (float, optional): The width of the line. Default is ``1.0``. - * color (rgb tuple or hex string, optional): The color of the line. Default is black. - * text (str, optional): The text of the label. Default is ``None``. - * textcolor (rgb tuple or hex string, optional): Color of the label text. Default is black. - * fontsize (int, optional): The size of the font of the label text. Default is ``12``. - - Returns - ------- - object - The matplotlib line collection object. - - """ - return draw_xlines_xy(lines, self.axes) - - def draw_polylines(self, polylines): - """Draw polylines on a 2D plot. - - Parameters - ---------- - polylines : list of dict - A list of dictionaries containing the polyline properties. - The following properties are supported: - - * points (list): XY(Z) coordinates of the polygon vertices. - * text (str, optional): The text of the label. Default is ``None``. - * textcolor (rgb tuple or hex string, optional): Color of the label text. Default is black. - * fontsize (int, optional): The size of the font of the label text. Default is ``12``. - * facecolor (rgb tuple or hex string, optional): Color of the polygon face. Default is white. - * edgecolor (rgb tuple or hex string, optional): Color of the edge of the polygon. Default is black. - * edgewidth (float): Width of the polygon edge. Default is ``1.0``. - - Returns - ------- - object - The matplotlib polyline collection object. - - """ - return draw_xpolylines_xy(polylines, self.axes) - - def draw_polygons(self, polygons): - """Draws polygons on a 2D plot. - - Parameters - ---------- - polygons : list of dict - List of dictionaries containing the polygon properties. - The following properties can be specified in the dict. - - * points (list): XY(Z) coordinates of the polygon vertices. - * text (str, optional): The text of the label. Default is ``None``. - * textcolor (rgb tuple or hex string, optional): Color of the label text. Default is black. - * fontsize (int, optional): The size of the font of the label text. Default is ``12``. - * facecolor (rgb tuple or hex string, optional): Color of the polygon face. Default is white. - * edgecolor (rgb tuple or hex string, optional): Color of the edge of the polygon. Default is black. - * edgewidth (float): Width of the polygon edge. Default is ``1.0``. - - Returns - ------- - object - The matplotlib polygon collection object. - - """ - return draw_xpolygons_xy(polygons, self.axes) - - def draw_arrows(self, arrows): - """Draws arrows on a 2D plot. - - Parameters - ---------- - arrows : list of dict - List of dictionaries containing the arrow properties. - The following properties of an arrow can be specified in the dict. - - * start (list): XY(Z) coordinates of the starting point. - * end (list): XY(Z) coordinates of the end point. - * text (str, optional): The text of the label. Default is ``None``. - * textcolor (rgb tuple or hex string, optional): Color of the label text. Default is black. - * fontsize (int, optional): The size of the font of the label text. Default is ``6``. - * color (rgb tuple or hex string, optional): Color of the arrow. Default is black. - * width (float): Width of the arrow. Default is ``1.0``. - - Returns - ------- - object - The matplotlib arrow collection object. - - """ - return draw_xarrows_xy(arrows, self.axes) - - def draw_arrows2(self, arrows): - for data in arrows: - a = data['start'][:2] - b = data['end'][:2] - color = data.get('color', (0.0, 0.0, 0.0)) - style = ArrowStyle("Simple, head_length=.1, head_width=.1, tail_width=.02") - arrow = FancyArrowPatch(a, b, - arrowstyle=style, - edgecolor=color, - facecolor=color, - zorder=2000, - mutation_scale=100) - self.axes.add_patch(arrow) - - def update(self, pause=0.0001): - """Updates and pauses the plot. - - Parameters - ---------- - pause : float - Amount of time to pause the plot in seconds. - - """ - self.axes.autoscale() - if self.tight: - plt.tight_layout() - plt.pause(pause) - - def update_pointcollection(self, collection, centers, radius=1.0): - """Updates the location and radii of a point collection. - - Parameters - ---------- - collection : object - The point collection to update. - centers : list - List of tuples or lists with XY(Z) location for the points in the collection. - radius : float or list, optional - The radii of the points. If a floar is given it will be used for all points. - - """ - try: - len(radius) - except Exception: - radius = [radius] * len(centers) - data = zip(centers, radius) - circles = [Circle(c[0:2], r) for c, r in data] - collection.set_paths(circles) - - def update_linecollection(self, collection, segments): - """Updates a line collection. - - Parameters - ---------- - collection : object - The line collection to update. - segments : list - List of tuples or lists with XY(Z) location for the start and end - points in each line in the collection. - - """ - collection.set_segments([(start[0:2], end[0:2]) for start, end in segments]) - - def update_polygoncollection(self, collection, polygons): - raise NotImplementedError diff --git a/src/compas_plotters/artists/__init__.py b/src/compas_plotters/artists/__init__.py index 81a555dad324..950a9adcc859 100644 --- a/src/compas_plotters/artists/__init__.py +++ b/src/compas_plotters/artists/__init__.py @@ -6,8 +6,8 @@ .. currentmodule:: compas_plotters.artists -Classes -======= +Primitive Artists +================= .. autosummary:: :toctree: generated/ @@ -21,6 +21,9 @@ CircleArtist EllipseArtist +Datastructure Artists +===================== + .. autosummary:: :toctree: generated/ :nosignatures: @@ -28,7 +31,23 @@ MeshArtist NetworkArtist + +Base Classes +============ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + PlotterArtist + """ +import inspect + +from compas.plugins import plugin +from compas.artists import Artist +from compas.artists import DataArtistNotRegistered + from compas.geometry import Point from compas.geometry import Vector from compas.geometry import Line @@ -40,7 +59,7 @@ from compas.datastructures import Mesh from compas.datastructures import Network -from .artist import Artist +from .artist import PlotterArtist from .pointartist import PointArtist from .vectorartist import VectorArtist from .lineartist import LineArtist @@ -48,24 +67,61 @@ from .polygonartist import PolygonArtist from .circleartist import CircleArtist from .ellipseartist import EllipseArtist - from .meshartist import MeshArtist from .networkartist import NetworkArtist -Artist.register(Point, PointArtist) -Artist.register(Vector, VectorArtist) -Artist.register(Line, LineArtist) -Artist.register(Polyline, PolylineArtist) -Artist.register(Polygon, PolygonArtist) -Artist.register(Circle, CircleArtist) -Artist.register(Ellipse, EllipseArtist) -Artist.register(Mesh, MeshArtist) -Artist.register(Network, NetworkArtist) +def verify_not_blender(): + try: + import bpy # noqa: F401 + except ImportError: + return True + else: + return False + + +artists_registered = False + + +@plugin(category='factories', pluggable_name='new_artist', trylast=True, requires=['matplotlib', verify_not_blender]) +def new_artist_plotter(cls, *args, **kwargs): + # "lazy registration" seems necessary to avoid item-artist pairs to be overwritten unintentionally + global artists_registered + + if not artists_registered: + PlotterArtist.register(Point, PointArtist) + PlotterArtist.register(Vector, VectorArtist) + PlotterArtist.register(Line, LineArtist) + PlotterArtist.register(Polyline, PolylineArtist) + PlotterArtist.register(Polygon, PolygonArtist) + PlotterArtist.register(Circle, CircleArtist) + PlotterArtist.register(Ellipse, EllipseArtist) + PlotterArtist.register(Mesh, MeshArtist) + PlotterArtist.register(Network, NetworkArtist) + artists_registered = True + + data = args[0] + + if 'artist_type' in kwargs: + cls = kwargs['artist_type'] + else: + dtype = type(data) + if dtype not in PlotterArtist.ITEM_ARTIST: + raise DataArtistNotRegistered('No Plotter artist is registered for this data type: {}'.format(dtype)) + cls = PlotterArtist.ITEM_ARTIST[dtype] + + # TODO: move this to the plugin module and/or to a dedicated function + + for name, value in inspect.getmembers(cls): + if inspect.isfunction(value): + if hasattr(value, '__isabstractmethod__'): + raise Exception('Abstract method not implemented: {}'.format(value)) + + return super(Artist, cls).__new__(cls) __all__ = [ - 'Artist', + 'PlotterArtist', 'PointArtist', 'VectorArtist', 'LineArtist', diff --git a/src/compas_plotters/artists/artist.py b/src/compas_plotters/artists/artist.py index f18076bc7048..f38a49e75d1b 100644 --- a/src/compas_plotters/artists/artist.py +++ b/src/compas_plotters/artists/artist.py @@ -1,31 +1,21 @@ -from abc import ABC -from abc import abstractmethod from abc import abstractproperty -_ITEM_ARTIST = {} +from compas.artists import Artist -class Artist(ABC): +class PlotterArtist(Artist): """Base class for all plotter artists.""" - def __init__(self, item): - self.plotter = None - self.item = item + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._plotter = None - @staticmethod - def register(item_type, artist_type): - _ITEM_ARTIST[item_type] = artist_type - - @staticmethod - def build(item, **kwargs): - artist_type = _ITEM_ARTIST[type(item)] - artist = artist_type(item, **kwargs) - return artist - - @staticmethod - def build_as(item, artist_type, **kwargs): - artist = artist_type(item, **kwargs) - return artist + @property + def plotter(self): + if not self._plotter: + from compas_plotters import Plotter + self._plotter = Plotter() + return self._plotter def viewbox(self): xlim = self.plotter.axes.get_xlim() @@ -38,13 +28,5 @@ def viewbox(self): def data(self): raise NotImplementedError - @abstractmethod - def draw(self): - pass - - @abstractmethod - def redraw(self): - pass - - def update_data(self): - raise NotImplementedError + def update_data(self) -> None: + self.plotter.axes.update_datalim(self.data) diff --git a/src/compas_plotters/artists/circleartist.py b/src/compas_plotters/artists/circleartist.py index 75c5994f9d96..672aee59c0af 100644 --- a/src/compas_plotters/artists/circleartist.py +++ b/src/compas_plotters/artists/circleartist.py @@ -1,17 +1,20 @@ -from typing import Tuple, List +from typing import Tuple +from typing import List +from typing import Any from typing_extensions import Literal + from matplotlib.patches import Circle as CirclePatch from compas.geometry import Circle -from compas_plotters.artists import Artist + +from compas.artists import PrimitiveArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class CircleArtist(Artist): +class CircleArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS circles.""" - zorder: int = 1000 - def __init__(self, circle: Circle, linewidth: float = 1.0, @@ -19,16 +22,28 @@ def __init__(self, facecolor: Color = (1.0, 1.0, 1.0), edgecolor: Color = (0, 0, 0), fill: bool = True, - alpha: float = 1.0): - super(CircleArtist, self).__init__(circle) + alpha: float = 1.0, + zorder: int = 1000, + **kwargs: Any): + + super().__init__(primitive=circle, **kwargs) + self._mpl_circle = None - self.circle = circle self.linewidth = linewidth self.linestyle = linestyle self.facecolor = facecolor self.edgecolor = edgecolor self.fill = fill self.alpha = alpha + self.zorder = zorder + + @property + def circle(self): + return self.primitive + + @circle.setter + def circle(self, circle): + self.primitive = circle @property def data(self) -> List[List[float]]: @@ -44,9 +59,6 @@ def data(self) -> List[List[float]]: points[3][1] += self.circle.radius return points - def update_data(self) -> None: - self.plotter.axes.update_datalim(self.data) - def draw(self) -> None: circle = CirclePatch( self.circle.center[:2], diff --git a/src/compas_plotters/artists/ellipseartist.py b/src/compas_plotters/artists/ellipseartist.py index 256bceb8b60f..8a713527da6a 100644 --- a/src/compas_plotters/artists/ellipseartist.py +++ b/src/compas_plotters/artists/ellipseartist.py @@ -1,17 +1,20 @@ -from typing import Tuple, List +from typing import Tuple +from typing import List +from typing import Any from typing_extensions import Literal + from matplotlib.patches import Ellipse as EllipsePatch from compas.geometry import Ellipse -from compas_plotters.artists import Artist + +from compas.artists import PrimitiveArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class EllipseArtist(Artist): +class EllipseArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS ellipses.""" - zorder: int = 1000 - def __init__(self, ellipse: Ellipse, linewidth: float = 1.0, @@ -19,16 +22,28 @@ def __init__(self, facecolor: Color = (1.0, 1.0, 1.0), edgecolor: Color = (0, 0, 0), fill: bool = True, - alpha: float = 1.0): - super(EllipseArtist, self).__init__(ellipse) + alpha: float = 1.0, + zorder: int = 1000, + **kwargs: Any): + + super().__init__(primitive=ellipse, **kwargs) + self._mpl_ellipse = None - self.ellipse = ellipse self.linewidth = linewidth self.linestyle = linestyle self.facecolor = facecolor self.edgecolor = edgecolor self.fill = fill self.alpha = alpha + self.zorder = zorder + + @property + def ellipse(self): + return self.primitive + + @ellipse.setter + def ellipse(self, ellipse): + self.primitive = ellipse @property def data(self) -> List[List[float]]: @@ -44,9 +59,6 @@ def data(self) -> List[List[float]]: points[3][1] += self.ellipse.minor return points - def update_data(self) -> None: - self.plotter.axes.update_datalim(self.data) - def draw(self) -> None: ellipse = EllipsePatch( self.ellipse.center[:2], diff --git a/src/compas_plotters/artists/lineartist.py b/src/compas_plotters/artists/lineartist.py index acf0d356d9c5..0eb44229dbb3 100644 --- a/src/compas_plotters/artists/lineartist.py +++ b/src/compas_plotters/artists/lineartist.py @@ -1,36 +1,51 @@ -from typing import Tuple, List +from typing import Tuple +from typing import List +from typing import Any from typing_extensions import Literal + from matplotlib.lines import Line2D from compas.geometry import Point, Line from compas.geometry import intersection_line_box_xy -from compas_plotters.artists import Artist + +from compas.artists import PrimitiveArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class LineArtist(Artist): +class LineArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS lines.""" - zorder: int = 1000 - def __init__(self, line: Line, draw_points: bool = False, draw_as_segment: bool = False, linewidth: float = 1.0, linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid', - color: Color = (0, 0, 0)): - super(LineArtist, self).__init__(line) + color: Color = (0, 0, 0), + zorder: int = 1000, + **kwargs: Any): + + super().__init__(primitive=line, **kwargs) + self._mpl_line = None self._start_artist = None self._end_artist = None self._segment_artist = None self.draw_points = draw_points self.draw_as_segment = draw_as_segment - self.line = line self.linewidth = linewidth self.linestyle = linestyle self.color = color + self.zorder = zorder + + @property + def line(self): + return self.primitive + + @line.setter + def line(self, line): + self.primitive = line def clip(self) -> List[Point]: """Compute the clipping points of the line for the current view box.""" @@ -74,7 +89,7 @@ def draw(self) -> None: self._end_artist = self.plotter.add(self.line.end, edgecolor=self.color) def redraw(self) -> None: - if self._draw_as_segment: + if self.draw_as_segment: x0, y0 = self.line.start[:2] x1, y1 = self.line.end[:2] self._mpl_line.set_xdata([x0, x1]) diff --git a/src/compas_plotters/artists/meshartist.py b/src/compas_plotters/artists/meshartist.py index b13a90780028..6bf4aac88eb3 100644 --- a/src/compas_plotters/artists/meshartist.py +++ b/src/compas_plotters/artists/meshartist.py @@ -1,187 +1,325 @@ -from typing import Dict, Tuple, List, Union +from typing import Dict +from typing import Tuple +from typing import List +from typing import Union +from typing import Optional +from typing import Any from typing_extensions import Literal from matplotlib.collections import LineCollection, PatchCollection from matplotlib.patches import Polygon as PolygonPatch from matplotlib.patches import Circle from compas.datastructures import Mesh -from compas_plotters.artists import Artist +from compas.artists import MeshArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class MeshArtist(Artist): - """Artist for COMPAS mesh data structures.""" +class MeshArtist(PlotterArtist, MeshArtist): + """Artist for COMPAS mesh data structures. - default_vertexcolor: Color = (1, 1, 1) - default_edgecolor: Color = (0, 0, 0) - default_facecolor: Color = (0.9, 0.9, 0.9) + Parameters + ---------- + mesh : :class:`compas.datastructures.Mesh` + A COMPAS mesh. + vertices : list of int, optional + A list of vertex identifiers. + Default is ``None``, in which case all vertices are drawn. + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + faces : list, optional + A list of face identifiers. + The default is ``None``, in which case all faces are drawn. + vertexcolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the vertices. + edgecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + facecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the faces. + show_vertices : bool, optional + show_edges : bool, optional + show_faces : bool, optional + vertexsize : int, optional + sizepolicy : {'relative', 'absolute'}, optional - default_vertexsize: int = 5 - default_edgewidth: float = 1.0 + Attributes + ---------- + vertexcollection : :class:`PatchCollection` + The collection containing the vertices. + edgecollection : :class:`LineCollection` + The collection containing the edges. + facecollection : :class:`PatchCollection` + The collection containing the faces. - zorder_faces: int = 1000 - zorder_edges: int = 2000 - zorder_vertices: int = 3000 + Class Attributes + ---------------- + zorder_vertices : int + zorder_edges : int + zorder_faces : int + """ def __init__(self, mesh: Mesh, + vertices: Optional[List[int]] = None, + edges: Optional[List[int]] = None, + faces: Optional[List[int]] = None, + vertexcolor: Color = (1, 1, 1), + edgecolor: Color = (0, 0, 0), + facecolor: Color = (0.9, 0.9, 0.9), + edgewidth: float = 1.0, show_vertices: bool = True, show_edges: bool = True, show_faces: bool = True, vertexsize: int = 5, sizepolicy: Literal['relative', 'absolute'] = 'relative', - vertexcolor: Color = (1, 1, 1), - edgewidth: float = 1.0, - edgecolor: Color = (0, 0, 0), - facecolor: Color = (0.9, 0.9, 0.9)): - super(MeshArtist, self).__init__(mesh) - self._mpl_vertex_collection = None - self._mpl_edge_collection = None - self._mpl_face_collection = None - self._vertexcolor = None - self._edgecolor = None - self._facecolor = None - self._edgewidth = None - self.mesh = mesh + zorder: int = 1000, + **kwargs: Any): + + super().__init__(mesh=mesh, **kwargs) + + self.sizepolicy = sizepolicy + + self.vertices = vertices + self.edges = edges + self.faces = faces + self.vertex_color = vertexcolor + self.vertex_size = vertexsize + self.edge_color = edgecolor + self.edge_width = edgewidth + self.face_color = facecolor self.show_vertices = show_vertices self.show_edges = show_edges self.show_faces = show_faces - self.vertexsize = vertexsize - self.sizepolicy = sizepolicy - self.vertexcolor = vertexcolor - self.edgewidth = edgewidth - self.edgecolor = edgecolor - self.facecolor = facecolor + self.zorder = zorder @property - def vertexcolor(self) -> Dict[int, Color]: - """dict: Vertex colors.""" - return self._vertexcolor - - @vertexcolor.setter - def vertexcolor(self, vertexcolor: Union[Color, Dict[int, Color]]): - if isinstance(vertexcolor, dict): - self._vertexcolor = vertexcolor - elif len(vertexcolor) == 3 and all(isinstance(c, (int, float)) for c in vertexcolor): - self._vertexcolor = {vertex: vertexcolor for vertex in self.mesh.vertices()} - else: - self._vertexcolor = {} + def vertex_size(self): + if not self._vertex_size: + factor = self.plotter.dpi if self.sizepolicy == 'absolute' else self.mesh.number_of_vertices() + size = self.default_vertexsize / factor + self._vertex_size = {vertex: size for vertex in self.mesh.vertices()} + return self._vertex_size + + @vertex_size.setter + def vertex_size(self, vertexsize): + factor = self.plotter.dpi if self.sizepolicy == 'absolute' else self.mesh.number_of_vertices() + if isinstance(vertexsize, dict): + self.vertex_size.update({vertex: size / factor for vertex, size in vertexsize.items()}) + elif isinstance(vertexsize, (int, float)): + self._vertex_size = {vertex: vertexsize / factor for vertex in self.mesh.vertices()} @property - def edgecolor(self) -> Dict[Tuple[int, int], Color]: - """dict: Edge colors.""" - return self._edgecolor - - @edgecolor.setter - def edgecolor(self, edgecolor: Union[Color, Dict[Tuple[int, int], Color]]): - if isinstance(edgecolor, dict): - self._edgecolor = edgecolor - elif len(edgecolor) == 3 and all(isinstance(c, (int, float)) for c in edgecolor): - self._edgecolor = {edge: edgecolor for edge in self.mesh.edges()} - else: - self._edgecolor = {} + def zorder_faces(self): + return self.zorder + 10 @property - def facecolor(self) -> Dict[int, Color]: - """dict: Face colors.""" - return self._facecolor - - @facecolor.setter - def facecolor(self, facecolor: Union[Color, Dict[int, Color]]): - if isinstance(facecolor, dict): - self._facecolor = facecolor - elif len(facecolor) == 3 and all(isinstance(c, (int, float)) for c in facecolor): - self._facecolor = {face: facecolor for face in self.mesh.faces()} - else: - self._facecolor = {} + def zorder_edges(self): + return self.zorder + 20 @property - def edgewidth(self) -> Dict[Tuple[int, int], float]: - """dict: Edge widths.""" - return self._edgewidth - - @edgewidth.setter - def edgewidth(self, edgewidth: Union[float, Dict[Tuple[int, int], float]]): - if isinstance(edgewidth, dict): - self._edgewidth = edgewidth - elif isinstance(edgewidth, (int, float)): - self._edgewidth = {edge: edgewidth for edge in self.mesh.edges()} - else: - self._edgewidth = {} + def zorder_vertices(self): + return self.zorder + 30 + + @property + def item(self): + """Mesh: Alias for ``~MeshArtist.mesh``""" + return self.mesh + + @item.setter + def item(self, item: Mesh): + self.mesh = item @property def data(self) -> List[List[float]]: return self.mesh.vertices_attributes('xy') - def draw(self) -> None: - """Draw the mesh.""" - vertex_xy = {vertex: self.mesh.vertex_attributes(vertex, 'xy') for vertex in self.mesh.vertices()} + # ============================================================================== + # clear and draw + # ============================================================================== - if self.show_faces: - polygons = [] - facecolors = [] - edgecolors = [] - linewidths = [] - for face in self.mesh.faces(): - data = [vertex_xy[vertex] for vertex in self.mesh.face_vertices(face)] - polygons.append(PolygonPatch(data)) - facecolors.append(self.facecolor.get(face, self.default_facecolor)) - edgecolors.append((0, 0, 0)) - linewidths.append(0.1) - collection = PatchCollection( - polygons, - facecolors=facecolors, - edgecolors=edgecolors, - lw=linewidths, - alpha=1.0, - linestyle='solid', - zorder=self.zorder_faces - ) - self.plotter.axes.add_collection(collection) - self._mpl_face_collection = collection + def clear_vertices(self) -> None: + if self._vertexcollection: + self.plotter.axes.remove_collection(self._vertexcollection) + self._vertexcollection = None - if self.show_edges: - lines = [] - colors = [] - widths = [] - for edge in self.mesh.edges(): - lines.append([vertex_xy[edge[0]], vertex_xy[edge[1]]]) - colors.append(self.edgecolor.get(edge, self.default_edgecolor)) - widths.append(self.edgewidth.get(edge, self.default_edgewidth)) - collection = LineCollection( - lines, - linewidths=widths, - colors=colors, - linestyle='solid', - alpha=1.0, - zorder=self.zorder_edges - ) - self.plotter.axes.add_collection(collection) - self._mpl_edge_collection = collection + def clear_edges(self) -> None: + if self._edgecollection: + self.plotter.axes.remove_collection(self._edgecollection) + self._edgecollection = None + def clear_faces(self) -> None: + if self._facecollection: + self.plotter.axes.remove_collection(self._facecollection) + self._facecollection = None + + def draw(self, + vertices: Optional[List[int]] = None, + edges: Optional[List[Tuple[int, int]]] = None, + faces: Optional[List[int]] = None, + vertexcolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + edgecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + facecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None + ) -> None: + """Draw the mesh. + + Parameters + ---------- + vertices : list of int, optional + A list of vertex identifiers. + Default is ``None``, in which case all vertices are drawn. + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + faces : list, optional + A list of face identifiers. + The default is ``None``, in which case all faces are drawn. + vertexcolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the vertices. + edgecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + facecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the faces. + """ + self.clear() if self.show_vertices: - if self.sizepolicy == 'absolute': - size = self.vertexsize / self.plotter.dpi - else: - size = self.vertexsize / self.mesh.number_of_vertices() - circles = [] - for vertex in self.mesh.vertices(): - x, y = vertex_xy[vertex] - circle = Circle( - [x, y], - radius=size, - facecolor=self.vertexcolor.get(vertex, self.default_vertexcolor), - edgecolor=(0, 0, 0), - lw=0.3, - ) - circles.append(circle) - collection = PatchCollection( - circles, - match_original=True, - zorder=self.zorder_vertices, - alpha=1.0 + self.draw_vertices(vertices=vertices, color=vertexcolor) + if self.show_edges: + self.draw_edges(edges=edges, color=edgecolor) + if self.show_faces: + self.draw_faces(faces=faces, color=facecolor) + + def draw_vertices(self, + vertices: Optional[List[int]] = None, + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + text: Optional[Dict[int, str]] = None) -> None: + """Draw a selection of vertices. + + Parameters + ---------- + vertices : list of int, optional + A list of vertex identifiers. + Default is ``None``, in which case all vertices are drawn. + color : rgb-tuple or dict of rgb-tuples, optional + The color specification for the vertices. + + Returns + ------- + None + """ + self.clear_vertices() + if vertices: + self.vertices = vertices + if color: + self.vertex_color = color + + circles = [] + for vertex in self.vertices: + x, y = self.vertex_xyz[vertex][:2] + circle = Circle( + [x, y], + radius=self.vertex_size.get(vertex, self.default_vertexsize), + facecolor=self.vertex_color.get(vertex, self.default_vertexcolor), + edgecolor=(0, 0, 0), + lw=0.3, ) - self.plotter.axes.add_collection(collection) + circles.append(circle) + + collection = PatchCollection( + circles, + match_original=True, + zorder=self.zorder_vertices, + alpha=1.0 + ) + self.plotter.axes.add_collection(collection) + self._vertexcollection = collection + + def draw_edges(self, + edges: Optional[List[Tuple[int, int]]] = None, + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + text: Optional[Dict[int, str]] = None) -> None: + """Draw a selection of edges. + + Parameters + ---------- + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + color : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + + Returns + ------- + None + """ + self.clear_edges() + if edges: + self.edges = edges + if color: + self.edge_color = color + + lines = [] + colors = [] + widths = [] + for edge in self.edges: + lines.append([self.vertex_xyz[edge[0]][:2], self.vertex_xyz[edge[1]][:2]]) + colors.append(self.edge_color.get(edge, self.default_edgecolor)) + widths.append(self.edge_width.get(edge, self.default_edgewidth)) + + collection = LineCollection( + lines, + linewidths=widths, + colors=colors, + linestyle='solid', + alpha=1.0, + zorder=self.zorder_edges + ) + self.plotter.axes.add_collection(collection) + self._edgecollection = collection + + def draw_faces(self, + faces: Optional[List[int]] = None, + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + text: Optional[Dict[int, str]] = None) -> None: + """Draw a selection of faces. + + Parameters + ---------- + faces : list, optional + A list of face identifiers. + The default is ``None``, in which case all faces are drawn. + color : rgb-tuple or dict of rgb-tuples, optional + The color specification for the faces. + + Returns + ------- + None + """ + self.clear_faces() + if faces: + self.faces = faces + if color: + self.face_color = color + + polygons = [] + facecolors = [] + edgecolors = [] + linewidths = [] + for face in self.faces: + data = [self.vertex_xyz[vertex][:2] for vertex in self.mesh.face_vertices(face)] + polygons.append(PolygonPatch(data)) + facecolors.append(self.face_color.get(face, self.default_facecolor)) + edgecolors.append((0, 0, 0)) + linewidths.append(0.1) - def redraw(self) -> None: - raise NotImplementedError + collection = PatchCollection( + polygons, + facecolors=facecolors, + edgecolors=edgecolors, + lw=linewidths, + alpha=1.0, + linestyle='solid', + zorder=self.zorder_faces + ) + self.plotter.axes.add_collection(collection) + self._facecollection = collection diff --git a/src/compas_plotters/artists/networkartist.py b/src/compas_plotters/artists/networkartist.py index 4e4f8c2e3272..3d29e2408c92 100644 --- a/src/compas_plotters/artists/networkartist.py +++ b/src/compas_plotters/artists/networkartist.py @@ -1,141 +1,220 @@ -from typing import Dict, Tuple, List, Union +from typing import Dict +from typing import Tuple +from typing import List +from typing import Union +from typing import Optional from typing_extensions import Literal + from matplotlib.collections import LineCollection, PatchCollection from matplotlib.patches import Circle + from compas.datastructures import Network -from compas_plotters.artists import Artist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class NetworkArtist(Artist): - """""" - - default_nodecolor: Color = (1, 1, 1) - default_edgecolor: Color = (0, 0, 0) - - default_nodesize: int = 5 - default_edgewidth: float = 1.0 - - zorder_edges: int = 2000 - zorder_nodes: int = 3000 +class NetworkArtist(PlotterArtist): + """Artist for COMPAS network data structures. + + Parameters + ---------- + network : :class:`compas.datastructures.Network` + A COMPAS network. + layer : str, optional + The parent layer of the network. + nodes : list of int, optional + A list of node identifiers. + Default is ``None``, in which case all nodes are drawn. + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + nodecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the nodes. + edgecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + show_nodes : bool, optional + show_edges : bool, optional + nodesize : int, optional + sizepolicy : {'relative', 'absolute'}, optional + + Attributes + ---------- + nodecollection : :class:`PatchCollection` + The collection containing the nodes. + edgecollection : :class:`LineCollection` + The collection containing the edges. + + Class Attributes + ---------------- + default_nodesize : int + default_edgewidth : float + zorder_nodes : int + zorder_edges : int + """ def __init__(self, network: Network, + nodes: Optional[List[int]] = None, + edges: Optional[List[int]] = None, + nodecolor: Color = (1, 1, 1), + edgecolor: Color = (0, 0, 0), + edgewidth: float = 1.0, show_nodes: bool = True, show_edges: bool = True, nodesize: int = 5, sizepolicy: Literal['relative', 'absolute'] = 'relative', - nodecolor: Color = (1, 1, 1), - edgewidth: float = 1.0, - edgecolor: Color = (0, 0, 0)): - super(NetworkArtist, self).__init__(network) - self._mpl_node_collection = None - self._mpl_edge_collection = None - self._nodecolor = None - self._edgecolor = None - self._edgewidth = None - self.network = network + zorder: int = 2000, + **kwargs): + + super().__init__(network=network, **kwargs) + + self.nodes = nodes + self.edges = edges + self.node_color = nodecolor + self.node_size = nodesize + self.edge_color = edgecolor + self.edge_width = edgewidth self.show_nodes = show_nodes self.show_edges = show_edges - self.nodesize = nodesize self.sizepolicy = sizepolicy - self.nodecolor = nodecolor - self.edgewidth = edgewidth - self.edgecolor = edgecolor + self.zorder = zorder @property - def nodecolor(self) -> Dict[int, Color]: - """dict: Vertex colors.""" - return self._nodecolor - - @nodecolor.setter - def nodecolor(self, nodecolor: Union[Color, Dict[int, Color]]): - if isinstance(nodecolor, dict): - self._nodecolor = nodecolor - elif len(nodecolor) == 3 and all(isinstance(c, (int, float)) for c in nodecolor): - self._nodecolor = {node: nodecolor for node in self.network.nodes()} - else: - self._nodecolor = {} + def zorder_edges(self): + return self.zorder @property - def edgecolor(self) -> Dict[Tuple[int, int], Color]: - """dict: Edge colors.""" - return self._edgecolor - - @edgecolor.setter - def edgecolor(self, edgecolor: Union[Color, Dict[Tuple[int, int], Color]]): - if isinstance(edgecolor, dict): - self._edgecolor = edgecolor - elif len(edgecolor) == 3 and all(isinstance(c, (int, float)) for c in edgecolor): - self._edgecolor = {edge: edgecolor for edge in self.network.edges()} - else: - self._edgecolor = {} + def zorder_nodes(self): + return self.zorder + 10 @property - def edgewidth(self) -> Dict[Tuple[int, int], float]: - """dict: Edge widths.""" - return self._edgewidth - - @edgewidth.setter - def edgewidth(self, edgewidth: Union[float, Dict[Tuple[int, int], float]]): - if isinstance(edgewidth, dict): - self._edgewidth = edgewidth - elif isinstance(edgewidth, (int, float)): - self._edgewidth = {edge: edgewidth for edge in self.network.edges()} - else: - self._edgewidth = {} + def item(self): + """Network: Alias for ``~NetworkArtist.network``""" + return self.network + + @item.setter + def item(self, item: Network): + self.network = item @property def data(self) -> List[List[float]]: return self.network.nodes_attributes('xy') - def draw(self) -> None: - """Draw the network.""" - node_xy = {node: self.network.node_attributes(node, 'xy') for node in self.network.nodes()} - + # ============================================================================== + # clear and draw + # ============================================================================== + + def clear_nodes(self): + if self._nodecollection: + self.plotter.axes.remove_collection(self._nodecollection) + self._nodecollection = None + + def clear_edges(self): + if self._edgecollection: + self.plotter.axes.remove_collection(self._edgecollection) + self._edgecollection = None + + def draw(self, + nodes: Optional[List[int]] = None, + edges: Optional[Tuple[int, int]] = None, + nodecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, + edgecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> None: + """Draw the network. + + Parameters + ---------- + nodes : list of int, optional + A list of node identifiers. + Default is ``None``, in which case all nodes are drawn. + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + nodecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the nodes. + edgecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + """ + self.clear() if self.show_nodes: - lines = [] - colors = [] - widths = [] - for edge in self.network.edges(): - lines.append([node_xy[edge[0]], node_xy[edge[1]]]) - colors.append(self.edgecolor.get(edge, self.default_edgecolor)) - widths.append(self.edgewidth.get(edge, self.default_edgewidth)) - collection = LineCollection( - lines, - linewidths=widths, - colors=colors, - linestyle='solid', - alpha=1.0, - zorder=self.zorder_edges + self.draw_nodes(nodes=nodes, color=nodecolor) + if self.show_edges: + self.draw_edges(edges=edges, color=edgecolor) + + def draw_nodes(self, + nodes: Optional[List[int]] = None, + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> None: + """Draw a selection of nodes. + + Parameters + ---------- + nodes : list of int, optional + A list of node identifiers. + Default is ``None``, in which case all nodes are drawn. + color : rgb-tuple or dict of rgb-tuples, optional + The color specification for the nodes. + """ + self.clear_nodes() + if nodes: + self.nodes = nodes + if color: + self.node_color = color + + circles = [] + for node in self.nodes: + x, y = self.node_xyz[node][:2] + circle = Circle( + [x, y], + radius=self.node_size.get(node, self.default_nodesize), + facecolor=self.node_color.get(node, self.default_nodecolor), + edgecolor=(0, 0, 0), + lw=0.3, ) - self.plotter.axes.add_collection(collection) - self._mpl_edge_collection = collection - - if self.show_nodes: - if self.sizepolicy == 'absolute': - size = self.nodesize / self.plotter.dpi - else: - size = self.nodesize / self.network.number_of_nodes() - circles = [] - for node in self.network.nodes(): - x, y = node_xy[node] - circle = Circle( - [x, y], - radius=size, - facecolor=self.nodecolor.get(node, self.default_nodecolor), - edgecolor=(0, 0, 0), - lw=0.3, - ) - circles.append(circle) - collection = PatchCollection( - circles, - match_original=True, - zorder=self.zorder_nodes, - alpha=1.0 - ) - self.plotter.axes.add_collection(collection) - - def redraw(self) -> None: - raise NotImplementedError + circles.append(circle) + + collection = PatchCollection( + circles, + match_original=True, + zorder=self.zorder_nodes, + alpha=1.0 + ) + self.plotter.axes.add_collection(collection) + self._nodecollection = collection + + def draw_edges(self, + edges: Optional[Tuple[int, int]] = None, + color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> None: + """Draw a selection of edges. + + Parameters + ---------- + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + color : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + """ + self.clear_edges() + if edges: + self.edges = edges + if color: + self.edge_color = color + + lines = [] + colors = [] + widths = [] + for edge in self.edges: + lines.append([self.node_xyz[edge[0]][:2], self.node_xyz[edge[1]][:2]]) + colors.append(self.edge_color.get(edge, self.default_edgecolor)) + widths.append(self.edge_width.get(edge, self.default_edgewidth)) + + collection = LineCollection( + lines, + linewidths=widths, + colors=colors, + linestyle='solid', + alpha=1.0, + zorder=self.zorder_edges + ) + self.plotter.axes.add_collection(collection) + self._edgecollection = collection diff --git a/src/compas_plotters/artists/pointartist.py b/src/compas_plotters/artists/pointartist.py index 3efc29442a94..f71e5ea1ec66 100644 --- a/src/compas_plotters/artists/pointartist.py +++ b/src/compas_plotters/artists/pointartist.py @@ -1,29 +1,44 @@ -from typing import Tuple, List +from typing import Tuple +from typing import List +from typing import Any + from matplotlib.patches import Circle from matplotlib.transforms import ScaledTranslation from compas.geometry import Point -from compas_plotters.artists import Artist + +from compas.artists import PrimitiveArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class PointArtist(Artist): +class PointArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS points.""" - zorder: int = 9000 - def __init__(self, point: Point, size: int = 5, facecolor: Color = (1.0, 1.0, 1.0), - edgecolor: Color = (0, 0, 0)): - super(PointArtist, self).__init__(point) + edgecolor: Color = (0, 0, 0), + zorder: int = 9000, + **kwargs: Any): + + super().__init__(primitive=point, **kwargs) + self._mpl_circle = None self._size = None - self.point = point self.size = size self.facecolor = facecolor self.edgecolor = edgecolor + self.zorder = zorder + + @property + def point(self): + return self.primitive + + @point.setter + def point(self, point): + self.primitive = point @property def _T(self): @@ -44,9 +59,6 @@ def size(self, size: int): def data(self) -> List[List[float]]: return [self.point[:2]] - def update_data(self) -> None: - self.plotter.axes.update_datalim(self.data) - def draw(self) -> None: circle = Circle( [0, 0], diff --git a/src/compas_plotters/artists/polygonartist.py b/src/compas_plotters/artists/polygonartist.py index 515de2eaf98c..58d32767a9f5 100644 --- a/src/compas_plotters/artists/polygonartist.py +++ b/src/compas_plotters/artists/polygonartist.py @@ -1,17 +1,20 @@ -from typing import Tuple, List +from typing import Tuple +from typing import List +from typing import Any from typing_extensions import Literal + from matplotlib.patches import Polygon as PolygonPatch from compas.geometry import Polygon -from compas_plotters.artists import Artist + +from compas.artists import PrimitiveArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class PolygonArtist(Artist): +class PolygonArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS polygons.""" - zorder: int = 1000 - def __init__(self, polygon: Polygon, linewidth: float = 1.0, @@ -19,16 +22,28 @@ def __init__(self, facecolor: Color = (1.0, 1.0, 1.0), edgecolor: Color = (0, 0, 0), fill: bool = True, - alpha: float = 1.0): - super(PolygonArtist, self).__init__(polygon) + alpha: float = 1.0, + zorder: int = 1000, + **kwargs: Any): + + super().__init__(primitive=polygon, **kwargs) + self._mpl_polygon = None - self.polygon = polygon self.linewidth = linewidth self.linestyle = linestyle self.facecolor = facecolor self.edgecolor = edgecolor self.fill = fill self.alpha = alpha + self.zorder = zorder + + @property + def polygon(self): + return self.primitive + + @polygon.setter + def polygon(self, polygon): + self.primitive = polygon @property def data(self) -> List[List[float]]: diff --git a/src/compas_plotters/artists/polylineartist.py b/src/compas_plotters/artists/polylineartist.py index dd123c6ed97f..68e9bc17980d 100644 --- a/src/compas_plotters/artists/polylineartist.py +++ b/src/compas_plotters/artists/polylineartist.py @@ -1,31 +1,46 @@ -from typing import Tuple, List +from typing import Tuple +from typing import List +from typing import Any from typing_extensions import Literal + from matplotlib.lines import Line2D from compas.geometry import Polyline -from compas_plotters.artists import Artist + +from compas.artists import PrimitiveArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class PolylineArtist(Artist): +class PolylineArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS polylines.""" - zorder: int = 1000 - def __init__(self, polyline: Polyline, draw_points: bool = True, linewidth: float = 1.0, linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid', - color: Color = (0, 0, 0)): - super(PolylineArtist, self).__init__(polyline) + color: Color = (0, 0, 0), + zorder: int = 1000, + **kwargs: Any): + + super().__init__(primitive=polyline, **kwargs) + self._mpl_line = None self._point_artists = [] self.draw_points = draw_points - self.polyline = polyline self.linewidth = linewidth self.linestyle = linestyle self.color = color + self.zorder = zorder + + @property + def polyline(self): + return self.primitive + + @polyline.setter + def polyline(self, polyline): + self.primitive = polyline @property def data(self) -> List[List[float]]: diff --git a/src/compas_plotters/artists/segmentartist.py b/src/compas_plotters/artists/segmentartist.py index 73b85af8342f..da4ce6af3945 100644 --- a/src/compas_plotters/artists/segmentartist.py +++ b/src/compas_plotters/artists/segmentartist.py @@ -1,32 +1,47 @@ -from typing import Tuple, List +from typing import Tuple +from typing import List +from typing import Any from typing_extensions import Literal + from matplotlib.lines import Line2D from compas.geometry import Line -from compas_plotters.artists import Artist + +from compas.artists import PrimitiveArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class SegmentArtist(Artist): +class SegmentArtist(PlotterArtist, PrimitiveArtist): """Artist for drawing COMPAS lines as segments.""" - zorder: int = 2000 - def __init__(self, line: Line, draw_points: bool = False, linewidth: float = 2.0, linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid', - color: Color = (0.0, 0.0, 0.0)): - super(SegmentArtist, self).__init__() + color: Color = (0.0, 0.0, 0.0), + zorder: int = 2000, + **kwargs: Any): + + super().__init__(primitive=line, **kwargs) + self._mpl_line = None self._start_artist = None self._end_artist = None self.draw_points = draw_points self.linestyle = linestyle self.linewidth = linewidth - self.line = line self.color = color + self.zorder = zorder + + @property + def line(self): + return self.primitive + + @line.setter + def line(self, line): + self.primitive = line @property def data(self) -> List[List[float]]: diff --git a/src/compas_plotters/artists/vectorartist.py b/src/compas_plotters/artists/vectorartist.py index 62cf93b24767..a150831c00b3 100644 --- a/src/compas_plotters/artists/vectorartist.py +++ b/src/compas_plotters/artists/vectorartist.py @@ -1,36 +1,52 @@ -from typing import Tuple, List, Optional +from typing import Tuple +from typing import List +from typing import Any +from typing import Optional + from matplotlib.patches import FancyArrowPatch from matplotlib.patches import ArrowStyle from compas.geometry import Point, Vector -from compas_plotters.artists import Artist + +from compas.artists import PrimitiveArtist +from .artist import PlotterArtist Color = Tuple[float, float, float] -class VectorArtist(Artist): +class VectorArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS vectors.""" - zorder: int = 3000 - def __init__(self, vector: Vector, point: Optional[Point] = None, draw_point: bool = False, - color: Color = (0, 0, 0)): - super(VectorArtist, self).__init__(vector) + color: Color = (0, 0, 0), + zorder: int = 3000, + **kwargs: Any): + + super().__init__(primitive=vector, **kwargs) + self._mpl_vector = None self._point_artist = None self.draw_point = draw_point self.point = point or Point(0.0, 0.0, 0.0) - self.vector = vector self.color = color + self.zorder = zorder + + @property + def vector(self): + return self.primitive + + @vector.setter + def vector(self, vector): + self.primitive = vector @property def data(self) -> List[List[float]]: return [self.point[:2], (self.point + self.vector)[:2]] def draw(self) -> None: - style = ArrowStyle("Simple, head_length=.1, head_width=.1, tail_width=.02") + style = ArrowStyle("Simple, head_length=0.1, head_width=0.1, tail_width=0.02") arrow = FancyArrowPatch(self.point[:2], (self.point + self.vector)[:2], arrowstyle=style, edgecolor=self.color, diff --git a/src/compas_plotters/geometryplotter.py b/src/compas_plotters/geometryplotter.py deleted file mode 100644 index 22d949ab7357..000000000000 --- a/src/compas_plotters/geometryplotter.py +++ /dev/null @@ -1,335 +0,0 @@ -import matplotlib.pyplot as plt - -from compas_plotters import Artist - -__all__ = ['GeometryPlotter'] - - -class GeometryPlotter: - """Plotter for the visualization of COMPAS geometry. - - Parameters - ---------- - view : tuple, optional - The area of the axes that should be zoomed into view. - DEfault is ``([-10, 10], [-3, 10])``. - figsize : tuple, optional - The size of the figure in inches. - Default is ``(8, 5)`` - - Attributes - ---------- - - Examples - -------- - - Notes - ----- - - """ - - def __init__(self, view=[(-8, 16), (-5, 10)], figsize=(8, 5), dpi=100, bgcolor=(1.0, 1.0, 1.0), show_axes=False): - self._show_axes = show_axes - self._bgcolor = None - self._viewbox = None - self._axes = None - self._artists = [] - self.viewbox = view - self.figsize = figsize - self.dpi = dpi - self.bgcolor = bgcolor - - @property - def viewbox(self): - """([xmin, xmax], [ymin, ymax]): The area of the axes that is zoomed into view.""" - return self._viewbox - - @viewbox.setter - def viewbox(self, view): - xlim, ylim = view - xmin, xmax = xlim - ymin, ymax = ylim - self._viewbox = [xmin, xmax], [ymin, ymax] - - @property - def axes(self): - """Returns the axes subplot matplotlib object. - - Returns - ------- - Axes - The matplotlib axes object. - - Notes - ----- - For more info, see the documentation of the Axes class ([1]_) and the - axis and tick API ([2]_). - - References - ---------- - .. [1] https://matplotlib.org/api/axes_api.html - .. [2] https://matplotlib.org/api/axis_api.html - - """ - if not self._axes: - figure = plt.figure(facecolor=self.bgcolor, - figsize=self.figsize, - dpi=self.dpi) - axes = figure.add_subplot(111, aspect='equal') - if self.viewbox: - xmin, xmax = self.viewbox[0] - ymin, ymax = self.viewbox[1] - axes.set_xlim(xmin, xmax) - axes.set_ylim(ymin, ymax) - axes.set_xscale('linear') - axes.set_yscale('linear') - if self._show_axes: - axes.set_frame_on(True) - # major_xticks = np.arange(0, 501, 20) - # major_yticks = np.arange(0, 301, 20) - # minor_xticks = np.arange(0, 501, 5) - # minor_yticks = np.arange(0, 301, 5) - # ax.tick_params(axis = 'both', which = 'major', labelsize = 6) - # ax.tick_params(axis = 'both', which = 'minor', labelsize = 0) - # ax.set_xticks(major_xticks) - # ax.set_xticks(minor_yticks, minor = True) - # ax.set_yticks(major_xticks) - # ax.set_yticks(minor_yticks, minor = True) - # axes.tick_params(labelbottom=False, labelleft=False) - # axes.grid(axis='both', linestyle='--', linewidth=0.5, color=(0.7, 0.7, 0.7)) - axes.grid(False) - axes.set_xticks([]) - axes.set_yticks([]) - axes.spines['top'].set_color('none') - axes.spines['right'].set_color('none') - axes.spines['left'].set_position('zero') - axes.spines['bottom'].set_position('zero') - axes.spines['left'].set_linestyle('-') - axes.spines['bottom'].set_linestyle('-') - else: - axes.grid(False) - axes.set_frame_on(False) - axes.set_xticks([]) - axes.set_yticks([]) - axes.autoscale_view() - plt.tight_layout() - self._axes = axes - return self._axes - - @property - def figure(self): - """Returns the matplotlib figure instance. - - Returns - ------- - Figure - The matplotlib figure instance. - - Notes - ----- - For more info, see the figure API ([1]_). - - References - ---------- - .. [1] https://matplotlib.org/2.0.2/api/figure_api.html - - """ - return self.axes.get_figure() - - @property - def canvas(self): - """Returns the canvas of the figure instance. - """ - return self.figure.canvas - - @property - def bgcolor(self): - """Returns the background color. - - Returns - ------- - str - The color as a string (hex colors). - - """ - return self._bgcolor - - @bgcolor.setter - def bgcolor(self, value): - """Sets the background color. - - Parameters - ---------- - value : str, tuple - The color specififcation for the figure background. - Colors should be specified in the form of a string (hex colors) or - as a tuple of normalized RGB components. - - """ - self._bgcolor = value - self.figure.set_facecolor(value) - - @property - def title(self): - """Returns the title of the plot. - - Returns - ------- - str - The title of the plot. - - """ - return self.figure.canvas.get_window_title() - - @title.setter - def title(self, value): - """Sets the title of the plot. - - Parameters - ---------- - value : str - The title of the plot. - - """ - self.figure.canvas.set_window_title(value) - - @property - def artists(self): - """list of :class:`compas_plotters.artists.Artist`""" - return self._artists - - @artists.setter - def artists(self, artists): - self._artists = artists - - # ========================================================================= - # Methods - # ========================================================================= - - def pause(self, pause): - if pause: - plt.pause(pause) - - def zoom_extents(self): - width, height = self.figsize - fig_aspect = width / height - data = [] - for artist in self.artists: - data += artist.data - x, y = zip(* data) - xmin = min(x) - xmax = max(x) - ymin = min(y) - ymax = max(y) - xspan = xmax - xmin - yspan = ymax - ymin - data_aspect = xspan / yspan - if data_aspect < fig_aspect: - scale = fig_aspect / data_aspect - self.axes.set_xlim(scale * (xmin - 0.1 * xspan), scale * (xmax + 0.1 * xspan)) - self.axes.set_ylim(ymin - 0.1 * yspan, ymax + 0.1 * yspan) - else: - scale = data_aspect / fig_aspect - self.axes.set_xlim(xmin - 0.1 * xspan, xmax + 0.1 * xspan) - self.axes.set_ylim(scale * (ymin - 0.1 * yspan), scale * (ymax + 0.1 * yspan)) - self.axes.autoscale_view() - - def add(self, item, artist=None, **kwargs): - if not artist: - artist = Artist.build(item, **kwargs) - artist.plotter = self - artist.draw() - self._artists.append(artist) - return artist - - def add_as(self, item, artist_type, **kwargs): - artist = Artist.build_as(item, artist_type, **kwargs) - artist.plotter = self - artist.draw() - self._artists.append(artist) - return artist - - def add_from_list(self, items, **kwargs): - artists = [] - for item in items: - artist = self.add(item, **kwargs) - artists.append(artist) - return artists - - def find(self, item): - for artist in self._artists: - if item is artist.item: - return artist - - def register_listener(self, listener): - """Register a listener for pick events. - - Parameters - ---------- - listener : callable - The handler for pick events. - - Returns - ------- - None - - Notes - ----- - For more information, see the docs of ``mpl_connect`` ([1]_), and on event - handling and picking ([2]_). - - References - ---------- - .. [1] https://matplotlib.org/api/backend_bases_api.html#matplotlib.backend_bases.FigureCanvasBase.mpl_connect - .. [2] https://matplotlib.org/users/event_handling.html - - """ - self.figure.canvas.mpl_connect('pick_event', listener) - - def draw(self, pause=None): - self.figure.canvas.draw() - self.figure.canvas.flush_events() - if pause: - plt.pause(pause) - - def redraw(self, pause=None): - """Updates and pauses the plot. - - Parameters - ---------- - pause : float - Ammount of time to pause the plot in seconds. - - """ - for artist in self._artists: - artist.redraw() - self.figure.canvas.draw() - self.figure.canvas.flush_events() - if pause: - plt.pause(pause) - - def show(self): - """Displays the plot. - - """ - self.draw() - plt.show() - - def save(self, filepath, **kwargs): - """Saves the plot to a file. - - Parameters - ---------- - filepath : str - Full path of the file. - - Notes - ----- - For an overview of all configuration options, see [1]_. - - References - ---------- - .. [1] https://matplotlib.org/2.0.2/api/pyplot_api.html#matplotlib.pyplot.savefig - - """ - plt.savefig(filepath, **kwargs) diff --git a/src/compas_plotters/meshplotter.py b/src/compas_plotters/meshplotter.py deleted file mode 100644 index c54503f590ad..000000000000 --- a/src/compas_plotters/meshplotter.py +++ /dev/null @@ -1,349 +0,0 @@ -from matplotlib.patches import Circle -from matplotlib.patches import Polygon - -from compas.utilities import color_to_rgb -from compas.utilities import pairwise - -from compas_plotters._plotter import BasePlotter, valuedict - - -__all__ = ['MeshPlotter'] - - -class MeshPlotter(BasePlotter): - """Plotter for the visualization of COMPAS meshes. - - Parameters - ---------- - mesh: object - The mesh to plot. - - Attributes - ---------- - title : str - Title of the plot. - mesh : object - The mesh to plot. - vertexcollection : object - The matplotlib collection for the mesh vertices. - edgecollection : object - The matplotlib collection for the mesh edges. - facecollection : object - The matplotlib collection for the mesh faces. - defaults : dict - Dictionary containing default attributes for vertices and edges. - - Examples - -------- - This is a basic example using the default settings for all visualization options. - For more detailed examples, see the documentation of the various drawing methods - listed below... - - .. plot:: - :include-source: - - import compas - from compas.datastructures import Mesh - from compas_plotters import MeshPlotter - - mesh = Mesh.from_obj(compas.get('faces.obj')) - - plotter = MeshPlotter(mesh) - plotter.draw_vertices(text='key', radius=0.15) - plotter.draw_edges() - plotter.draw_faces() - plotter.show() - - Notes - ----- - For more info about ``matplotlib``, see [1]_. - - References - ---------- - .. [1] Hunter, J. D., 2007. *Matplotlib: A 2D graphics environment*. - Computing In Science & Engineering (9) 3, p.90-95. - Available at: http://ieeexplore.ieee.org/document/4160265/citations. - - """ - - def __init__(self, mesh, **kwargs): - super().__init__(**kwargs) - self.title = 'MeshPlotter' - self.mesh = mesh - self.vertexcollection = None - self.edgecollection = None - self.facecollection = None - self.defaults = { - 'vertex.radius': 0.1, - 'vertex.facecolor': '#ffffff', - 'vertex.edgecolor': '#000000', - 'vertex.edgewidth': 0.5, - 'vertex.textcolor': '#000000', - 'vertex.fontsize': kwargs.get('fontsize', 10), - - 'edge.width': 1.0, - 'edge.color': '#000000', - 'edge.textcolor': '#000000', - 'edge.fontsize': kwargs.get('fontsize', 10), - - 'face.facecolor': '#eeeeee', - 'face.edgecolor': '#000000', - 'face.edgewidth': 0.1, - 'face.textcolor': '#000000', - 'face.fontsize': kwargs.get('fontsize', 10), - } - - def clear(self): - """Clears the mesh plotter vertices, edges and faces.""" - self.clear_vertices() - self.clear_edges() - self.clear_faces() - - def draw_vertices(self, keys=None, radius=None, text=None, - facecolor=None, edgecolor=None, edgewidth=None, - textcolor=None, fontsize=None, picker=None): - """Draws the mesh vertices. - - Parameters - ---------- - keys : list - The keys of the vertices to plot. - radius : {float, dict} - A list of radii for the vertices. - text : {{'index', 'key'}, str, dict} - Strings to be displayed on the vertices. - facecolor : {color, dict} - Color for the vertex circle fill. - edgecolor : {color, dict} - Color for the vertex circle edge. - edgewidth : {float, dict} - Width for the vertex circle edge. - textcolor : {color, dict} - Color for the text to be displayed on the vertices. - fontsize : {int, dict} - Font size for the text to be displayed on the vertices. - - Returns - ------- - object - The matplotlib vertex collection object. - """ - keys = keys or list(self.mesh.vertices()) - - if text == 'key': - text = {key: str(key) for key in self.mesh.vertices()} - elif text == 'index': - text = {key: str(index) for index, key in enumerate(self.mesh.vertices())} - elif isinstance(text, str): - if text in self.mesh.default_vertex_attributes: - default = self.mesh.default_vertex_attributes[text] - if isinstance(default, float): - text = {key: '{:.1f}'.format(attr[text]) for key, attr in self.mesh.vertices(True)} - else: - text = {key: str(attr[text]) for key, attr in self.mesh.vertices(True)} - - radiusdict = valuedict(keys, radius, self.defaults['vertex.radius']) - textdict = valuedict(keys, text, '') - facecolordict = valuedict(keys, facecolor, self.defaults['vertex.facecolor']) - edgecolordict = valuedict(keys, edgecolor, self.defaults['vertex.edgecolor']) - edgewidthdict = valuedict(keys, edgewidth, self.defaults['vertex.edgewidth']) - textcolordict = valuedict(keys, textcolor, self.defaults['vertex.textcolor']) - fontsizedict = valuedict(keys, fontsize, self.defaults['vertex.fontsize']) - - points = [] - for key in keys: - points.append({ - 'pos': self.mesh.vertex_coordinates(key, 'xy'), - 'radius': radiusdict[key], - 'text': textdict[key], - 'facecolor': facecolordict[key], - 'edgecolor': edgecolordict[key], - 'edgewidth': edgewidthdict[key], - 'textcolor': textcolordict[key], - 'fontsize': fontsizedict[key] - }) - - collection = self.draw_points(points) - self.vertexcollection = collection - - if picker: - collection.set_picker(picker) - return collection - - def clear_vertices(self): - """Clears the mesh plotter vertices.""" - if self.vertexcollection: - self.vertexcollection.remove() - - def update_vertices(self, radius=None): - """Updates the plotter vertex collection based on the current state of the mesh. - - Parameters - ---------- - radius : {float, dict}, optional - The vertex radius as a single value, which will be applied to all vertices, - or as a dictionary mapping vertex keys to specific radii. - Default is the value set in ``self.defaults``. - """ - radius = valuedict(self.mesh.vertices(), radius, self.defaults['vertex.radius']) - circles = [] - for key in self.mesh.vertices(): - c = self.mesh.vertex_coordinates(key, 'xy') - r = radius[key] - circles.append(Circle(c, r)) - self.vertexcollection.set_paths(circles) - - def draw_edges(self, keys=None, width=None, color=None, text=None, textcolor=None, fontsize=None): - """Draws the mesh edges. - - Parameters - ---------- - keys : list - The keys of the edges to plot. - width : {float, dict} - Width of the mesh edges. - color : {color, dict} - Color for the edge lines. - text : {{'index', 'key'}, str, dict} - Strings to be displayed on the edges. - textcolor : rgb tuple or dict of rgb tuples - Color for the text to be displayed on the edges. - fontsize : int or dict of int. - Font size for the text to be displayed on the edges. - - Returns - ------- - object - The matplotlib edge collection object. - - """ - keys = keys or list(self.mesh.edges()) - - if text == 'key': - text = {(u, v): '{}-{}'.format(u, v) for u, v in self.mesh.edges()} - elif text == 'index': - text = {(u, v): str(index) for index, (u, v) in enumerate(self.mesh.edges())} - else: - pass - - widthdict = valuedict(keys, width, self.defaults['edge.width']) - colordict = valuedict(keys, color, self.defaults['edge.color']) - textdict = valuedict(keys, text, '') - textcolordict = valuedict(keys, textcolor, self.defaults['edge.textcolor']) - fontsizedict = valuedict(keys, fontsize, self.defaults['edge.fontsize']) - - lines = [] - for u, v in keys: - lines.append({ - 'start': self.mesh.vertex_coordinates(u, 'xy'), - 'end': self.mesh.vertex_coordinates(v, 'xy'), - 'width': widthdict[(u, v)], - 'color': colordict[(u, v)], - 'text': textdict[(u, v)], - 'textcolor': textcolordict[(u, v)], - 'fontsize': fontsizedict[(u, v)] - }) - - collection = self.draw_lines(lines) - self.edgecollection = collection - return collection - - def clear_edges(self): - """Clears the mesh plotter edges.""" - if self.edgecollection: - self.edgecollection.remove() - - def update_edges(self): - """Updates the plotter edge collection based on the mesh.""" - segments = [] - for u, v in self.mesh.edges(): - segments.append([self.mesh.vertex_coordinates(u, 'xy'), self.mesh.vertex_coordinates(v, 'xy')]) - self.edgecollection.set_segments(segments) - - def highlight_path(self, path, edgecolor=None, edgetext=None, edgewidth=None): - lines = [] - for u, v in pairwise(path): - sp = self.mesh.vertex_coordinates(u, 'xy') - ep = self.mesh.vertex_coordinates(v, 'xy') - lines.append({ - 'start': sp, - 'end': ep, - 'width': edgewidth or self.defaults.get('edge.width', 2.0), - 'color': edgecolor or self.defaults.get('edge.color', '#ff0000') - }) - self.draw_lines(lines) - - def draw_faces(self, keys=None, text=None, - facecolor=None, edgecolor=None, edgewidth=None, textcolor=None, fontsize=None): - """Draws the mesh faces. - - Parameters - ---------- - keys : list - The keys of the edges to plot. - text : {{'index', 'key'}, str, dict} - Strings to be displayed on the edges. - facecolor : {color, dict} - Color for the face fill. - edgecolor : {color, dict} - Color for the face edge. - edgewidth : {float, dict} - Width for the face edge. - textcolor : {color, dict} - Color for the text to be displayed on the edges. - fontsize : {int, dict} - Font size for the text to be displayed on the edges. - - Returns - ------- - object - The matplotlib face collection object. - """ - keys = keys or list(self.mesh.faces()) - - if text == 'key': - text = {key: str(key) for key in self.mesh.faces()} - elif text == 'index': - text = {key: str(index) for index, key in enumerate(self.mesh.faces())} - else: - pass - - textdict = valuedict(keys, text, '') - facecolordict = valuedict(keys, facecolor, self.defaults['face.facecolor']) - edgecolordict = valuedict(keys, edgecolor, self.defaults['face.edgecolor']) - edgewidthdict = valuedict(keys, edgewidth, self.defaults['face.edgewidth']) - textcolordict = valuedict(keys, textcolor, self.defaults['face.textcolor']) - fontsizedict = valuedict(keys, fontsize, self.defaults['face.fontsize']) - - polygons = [] - for key in keys: - polygons.append({ - 'points': self.mesh.face_coordinates(key, 'xy'), - 'text': textdict[key], - 'facecolor': facecolordict[key], - 'edgecolor': edgecolordict[key], - 'edgewidth': edgewidthdict[key], - 'textcolor': textcolordict[key], - 'fontsize': fontsizedict[key] - }) - - collection = self.draw_polygons(polygons) - self.facecollection = collection - return collection - - def clear_faces(self): - """Clears the mesh plotter faces.""" - if self.facecollection: - self.facecollection.remove() - - def update_faces(self, facecolor=None): - """Updates the plotter face collection based on the mesh.""" - facecolor = valuedict(self.mesh.faces(), facecolor, self.defaults['face.facecolor']) - polygons = [] - facecolors = [] - for fkey in self.mesh.faces(): - points = self.mesh.face_coordinates(fkey, 'xy') - polygons.append(Polygon(points)) - facecolors.append(color_to_rgb(facecolor[fkey], normalize=True)) - self.facecollection.set_paths(polygons) - self.facecollection.set_facecolor(facecolors) diff --git a/src/compas_plotters/networkplotter.py b/src/compas_plotters/networkplotter.py deleted file mode 100644 index 0fcffc348162..000000000000 --- a/src/compas_plotters/networkplotter.py +++ /dev/null @@ -1,275 +0,0 @@ -from matplotlib.patches import Circle -from compas_plotters._plotter import BasePlotter, valuedict - - -__all__ = ['NetworkPlotter'] - - -class NetworkPlotter(BasePlotter): - """Plotter for the visualization of COMPAS Networks. - - Parameters - ---------- - network : :class:`compas.datastructures.Network` - The network to plot. - - Attributes - ---------- - title : str - Title of the plot. - network : object - The network to plot. - nodecollection : object - The matplotlib collection for the network nodes. - edgecollection : object - The matplotlib collection for the network edges. - defaults : dict - Dictionary containing default attributes for nodes and edges. - - Notes - ----- - For more info, see [1]_. - - References - ---------- - .. [1] Hunter, J. D., 2007. *Matplotlib: A 2D graphics environment*. - Computing In Science & Engineering (9) 3, p.90-95. - Available at: http://ieeexplore.ieee.org/document/4160265/citations. - - Examples - -------- - .. plot:: - :include-source: - - import compas - from compas.datastructures import Network - from compas_plotters import NetworkPlotter - - network = Network.from_obj(compas.get('lines.obj')) - - plotter = NetworkPlotter(network) - plotter.draw_nodes( - text='key', - facecolor={key: '#ff0000' for key in network.leaves()}, - radius=0.15 - ) - plotter.draw_edges() - plotter.show() - - """ - - def __init__(self, network, **kwargs): - super().__init__(**kwargs) - self.title = 'NetworkPlotter' - self.datastructure = network - self.nodecollection = None - self.edgecollection = None - self.defaults = { - 'node.radius': 0.1, - 'node.facecolor': '#ffffff', - 'node.edgecolor': '#000000', - 'node.edgewidth': 0.5, - 'node.textcolor': '#000000', - 'node.fontsize': kwargs.get('fontsize', 10), - - 'edge.width': 1.0, - 'edge.color': '#000000', - 'edge.textcolor': '#000000', - 'edge.fontsize': kwargs.get('fontsize', 10), - } - - def clear(self): - """Clears the network plotter edges and nodes.""" - self.clear_nodes() - self.clear_edges() - - def clear_nodes(self): - """Clears the netwotk plotter nodes.""" - if self.nodecollection: - self.nodecollection.remove() - - def clear_edges(self): - """Clears the network object edges.""" - if self.edgecollection: - self.edgecollection.remove() - - # def draw_as_lines(self, color=None, width=None): - # # if len(args) > 0: - # # return super(MeshPlotter, self).draw_lines(*args, **kwargs) - # lines = [] - # for u, v in self.datastructure.edges(): - # lines.append({ - # 'start': self.datastructure.node_coordinates(u, 'xy'), - # 'end': self.datastructure.node_coordinates(v, 'xy'), - # 'color': color, - # 'width': width, - # }) - # return super(NetworkPlotter, self).draw_lines(lines) - - def draw_nodes(self, - keys=None, - radius=None, - text=None, - facecolor=None, - edgecolor=None, - edgewidth=None, - textcolor=None, - fontsize=None, - picker=None): - """Draws the network nodes. - - Parameters - ---------- - keys : list - The keys of the nodes to plot. - radius : {float, dict} - A list of radii for the nodes. - text : {{'index', 'key'}, str, dict} - Strings to be displayed on the nodes. - facecolor : {color, dict} - Color for the node circle fill. - edgecolor : {color, dict} - Color for the node circle edge. - edgewidth : {float, dict} - Width for the node circle edge. - textcolor : {color, dict} - Color for the text to be displayed on the nodes. - fontsize : {int, dict} - Font size for the text to be displayed on the nodes. - - Returns - ------- - object - The matplotlib point collection object. - - """ - keys = keys or list(self.datastructure.nodes()) - - if text == 'key': - text = {key: str(key) for key in self.datastructure.nodes()} - elif text == 'index': - text = {key: str(index) for index, key in enumerate(self.datastructure.nodes())} - elif isinstance(text, str): - if text in self.datastructure.default_node_attributes: - default = self.datastructure.default_node_attributes[text] - if isinstance(default, float): - text = {key: '{:.1f}'.format(attr[text]) for key, attr in self.datastructure.nodes(True)} - else: - text = {key: str(attr[text]) for key, attr in self.datastructure.nodes(True)} - else: - pass - - radiusdict = valuedict(keys, radius, self.defaults['node.radius']) - textdict = valuedict(keys, text, '') - facecolordict = valuedict(keys, facecolor, self.defaults['node.facecolor']) - edgecolordict = valuedict(keys, edgecolor, self.defaults['node.edgecolor']) - edgewidthdict = valuedict(keys, edgewidth, self.defaults['node.edgewidth']) - textcolordict = valuedict(keys, textcolor, self.defaults['node.textcolor']) - fontsizedict = valuedict(keys, fontsize, self.defaults['node.fontsize']) - - points = [] - for key in keys: - points.append({ - 'pos': self.datastructure.node_coordinates(key, 'xy'), - 'radius': radiusdict[key], - 'text': textdict[key], - 'facecolor': facecolordict[key], - 'edgecolor': edgecolordict[key], - 'edgewidth': edgewidthdict[key], - 'textcolor': textcolordict[key], - 'fontsize': fontsizedict[key] - }) - - collection = self.draw_points(points) - self.nodecollection = collection - - if picker: - collection.set_picker(picker) - return collection - - def update_nodes(self, radius=0.1): - """Updates the plotter node collection based on the network.""" - circles = [] - for key in self.datastructure.nodes(): - center = self.datastructure.node_coordinates(key, 'xy') - circles.append(Circle(center, radius)) - self.nodecollection.set_paths(circles) - - def draw_edges(self, - keys=None, - width=None, - color=None, - text=None, - textcolor=None, - fontsize=None): - """Draws the network edges. - - Parameters - ---------- - keys : list - The keys of the edges to plot. - width : {float, dict} - Width of the network edges. - color : {color, dict} - Color for the edge lines. - text : {{'index', 'key'}, str, dict} - Strings to be displayed on the edges. - textcolor : {color, dict} - Color for the text to be displayed on the edges. - fontsize : {int, dict} - Font size for the text to be displayed on the edges. - - Returns - ------- - object - The matplotlib line collection object. - - """ - keys = keys or list(self.datastructure.edges()) - - if text == 'key': - text = {(u, v): '{}-{}'.format(u, v) for u, v in self.datastructure.edges()} - elif text == 'index': - text = {(u, v): str(index) for index, (u, v) in enumerate(self.datastructure.edges())} - else: - pass - - widthdict = valuedict(keys, width, self.defaults['edge.width']) - colordict = valuedict(keys, color, self.defaults['edge.color']) - textdict = valuedict(keys, text, '') - textcolordict = valuedict(keys, textcolor, self.defaults['edge.textcolor']) - fontsizedict = valuedict(keys, fontsize, self.defaults['edge.fontsize']) - - lines = [] - for u, v in keys: - lines.append({ - 'start': self.datastructure.node_coordinates(u, 'xy'), - 'end': self.datastructure.node_coordinates(v, 'xy'), - 'width': widthdict[(u, v)], - 'color': colordict[(u, v)], - 'text': textdict[(u, v)], - 'textcolor': textcolordict[(u, v)], - 'fontsize': fontsizedict[(u, v)] - }) - - collection = self.draw_lines(lines) - self.edgecollection = collection - return collection - - def update_edges(self): - """Updates the plotter edge collection based on the network.""" - segments = [] - for u, v in self.datastructure.edges(): - segments.append([self.datastructure.node_coordinates(u, 'xy'), self.datastructure.node_coordinates(v, 'xy')]) - self.edgecollection.set_segments(segments) - - # def draw_path(self, path): - # edges = [] - # for u, v in pairwise(path): - # if not network.has_edge(u, v): - # u, v = v, u - # edges.append((u, v)) - # self.draw_edges( - # color={(u, v): '#ff0000' for u, v in edges}, - # width={(u, v): 5.0 for u, v in edges} - # ) diff --git a/src/compas_plotters/plotter.py b/src/compas_plotters/plotter.py index b180b488a3e6..75af3852fa71 100644 --- a/src/compas_plotters/plotter.py +++ b/src/compas_plotters/plotter.py @@ -1,15 +1,25 @@ import os from typing import Callable, Optional, Tuple, List, Union +from typing_extensions import Literal import matplotlib import matplotlib.pyplot as plt import tempfile from PIL import Image import compas -from compas_plotters import Artist +from .artists import PlotterArtist -class Plotter: +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class Plotter(metaclass=Singleton): """Plotter for the visualization of COMPAS geometry. Parameters @@ -28,7 +38,8 @@ def __init__(self, figsize: Tuple[float, float] = (8.0, 5.0), dpi: float = 100, bgcolor: Tuple[float, float, float] = (1.0, 1.0, 1.0), - show_axes: bool = False): + show_axes: bool = False, + zstack: Literal['natural', 'zorder'] = 'zorder'): self._show_axes = show_axes self._bgcolor = None self._viewbox = None @@ -38,6 +49,7 @@ def __init__(self, self.figsize = figsize self.dpi = dpi self.bgcolor = bgcolor + self.zstack = zstack @property def viewbox(self) -> Tuple[Tuple[float, float], Tuple[float, float]]: @@ -182,12 +194,12 @@ def title(self, value: str): self.figure.canvas.set_window_title(value) @property - def artists(self) -> List[Artist]: - """list of :class:`compas_plotters.artists.Artist`""" + def artists(self) -> List[PlotterArtist]: + """list of :class:`compas_plotters.artists.PlotterArtist`""" return self._artists @artists.setter - def artists(self, artists: List[Artist]): + def artists(self, artists: List[PlotterArtist]): self._artists = artists # ========================================================================= @@ -221,14 +233,19 @@ def zoom_extents(self, padding: Optional[int] = None) -> None: xspan = xmax - xmin + padding yspan = ymax - ymin + padding data_aspect = xspan / yspan + xlim = [xmin - 0.1 * xspan, xmax + 0.1 * xspan] + ylim = [ymin - 0.1 * yspan, ymax + 0.1 * yspan] if data_aspect < fig_aspect: scale = fig_aspect / data_aspect - self.axes.set_xlim(scale * (xmin - 0.1 * xspan), scale * (xmax + 0.1 * xspan)) - self.axes.set_ylim(ymin - 0.1 * yspan, ymax + 0.1 * yspan) + xlim[0] *= scale + xlim[1] *= scale else: scale = data_aspect / fig_aspect - self.axes.set_xlim(xmin - 0.1 * xspan, xmax + 0.1 * xspan) - self.axes.set_ylim(scale * (ymin - 0.1 * yspan), scale * (ymax + 0.1 * yspan)) + ylim[0] *= scale + ylim[1] *= scale + self.viewbox = (xlim, ylim) + self.axes.set_xlim(*xlim) + self.axes.set_ylim(*ylim) self.axes.autoscale_view() def add(self, @@ -240,13 +257,16 @@ def add(self, compas.geometry.Polyline, compas.geometry.Vector, compas.datastructures.Mesh], - artist: Optional[Artist] = None, - **kwargs) -> Artist: + artist: Optional[PlotterArtist] = None, + **kwargs) -> PlotterArtist: """Add a COMPAS geometry object or data structure to the plot. """ if not artist: - artist = Artist.build(item, **kwargs) - artist.plotter = self + if self.zstack == 'natural': + zorder = 1000 + len(self._artists) * 100 + artist = PlotterArtist(item, zorder=zorder, **kwargs) + else: + artist = PlotterArtist(item, **kwargs) artist.draw() self._artists.append(artist) return artist @@ -260,16 +280,15 @@ def add_as(self, compas.geometry.Polyline, compas.geometry.Vector, compas.datastructures.Mesh], - artist_type: Artist, - **kwargs) -> Artist: + artist_type: PlotterArtist, + **kwargs) -> PlotterArtist: """Add a COMPAS geometry object or data structure using a specific artist type.""" - artist = Artist.build_as(item, artist_type, **kwargs) - artist.plotter = self + artist = PlotterArtist(item, artist_type=artist_type, **kwargs) artist.draw() self._artists.append(artist) return artist - def add_from_list(self, items, **kwargs) -> List[Artist]: + def add_from_list(self, items, **kwargs) -> List[PlotterArtist]: """Add multiple COMPAS geometry objects and/or data structures from a list.""" artists = [] for item in items: @@ -285,7 +304,7 @@ def find(self, compas.geometry.Polygon, compas.geometry.Polyline, compas.geometry.Vector, - compas.datastructures.Mesh]) -> Artist: + compas.datastructures.Mesh]) -> PlotterArtist: """Find a geometry object or data structure in the plot.""" for artist in self._artists: if item is artist.item: diff --git a/src/compas_rhino/__init__.py b/src/compas_rhino/__init__.py index 443dd01a5020..9901e6ffbec1 100644 --- a/src/compas_rhino/__init__.py +++ b/src/compas_rhino/__init__.py @@ -42,6 +42,11 @@ def clear(): delete_objects(guids, purge=True) # noqa: F405 +def redraw(): + rs.EnableRedraw(True) + rs.Redraw() + + def _check_rhino_version(version): supported_versions = ['5.0', '6.0', '7.0'] @@ -88,7 +93,7 @@ def _get_ironpython_lib_path_mac(version): lib_paths = { '5.0': ['/', 'Applications', 'Rhinoceros.app', 'Contents'], '6.0': ['/', 'Applications', 'Rhinoceros.app', 'Contents', 'Frameworks', 'RhCore.framework', 'Versions', 'A'], - '7.0': ['/', 'Applications', 'Rhino 7.app', 'Contents', 'Frameworks', 'RhCore.framework', 'Versions', 'A'] + '7.0': ['/', 'Applications', 'Rhinoceros.app', 'Contents', 'Frameworks', 'RhCore.framework', 'Versions', 'A'] } return os.path.join(*lib_paths.get(version) + ['Resources', 'ManagedPlugIns', 'RhinoDLR_Python.rhp', 'Lib']) @@ -213,4 +218,5 @@ def _try_remove_bootstrapper(path): 'compas_rhino.geometry.trimesh', 'compas_rhino.install', 'compas_rhino.uninstall', + 'compas_rhino.artists', ] diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 8c8eda7029da..4675726a820c 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -5,24 +5,6 @@ .. currentmodule:: compas_rhino.artists -.. rst-class:: lead - -Artists for visualising (painting) COMPAS objects in Rhino. -Artists convert COMPAS objects to Rhino geometry and data. - -.. code-block:: python - - import compas - from compas.datastructures import Mesh - from compas_rhino.artists import MeshArtist - - mesh = Mesh.from_off(compas.get('tubemesh.off')) - - artist = MeshArtist(mesh, layer='COMPAS::tubemesh.off') - - artist.clear_layer() - artist.draw() - Primitive Artists ================= @@ -86,13 +68,18 @@ :toctree: generated/ :nosignatures: - BaseArtist - PrimitiveArtist - ShapeArtist + RhinoArtist """ from __future__ import absolute_import +import inspect + +from compas.plugins import plugin +from compas.artists import Artist +from compas.artists import ShapeArtist +from compas.artists import DataArtistNotRegistered + from compas.geometry import Circle from compas.geometry import Frame from compas.geometry import Line @@ -115,11 +102,9 @@ from compas.datastructures import VolMesh from compas.robots import RobotModel +import compas_rhino -from ._artist import BaseArtist # noqa: F401 F403 -from ._primitiveartist import PrimitiveArtist # noqa: F401 F403 -from ._shapeartist import ShapeArtist # noqa: F401 - +from .artist import RhinoArtist from .circleartist import CircleArtist from .frameartist import FrameArtist from .lineartist import LineArtist @@ -128,7 +113,6 @@ from .polygonartist import PolygonArtist from .polylineartist import PolylineArtist from .vectorartist import VectorArtist - from .boxartist import BoxArtist from .capsuleartist import CapsuleArtist from .coneartist import ConeArtist @@ -136,41 +120,100 @@ from .polyhedronartist import PolyhedronArtist from .sphereartist import SphereArtist from .torusartist import TorusArtist - from .meshartist import MeshArtist from .networkartist import NetworkArtist from .volmeshartist import VolMeshArtist - from .robotmodelartist import RobotModelArtist -BaseArtist.register(Circle, CircleArtist) -BaseArtist.register(Frame, FrameArtist) -BaseArtist.register(Line, LineArtist) -BaseArtist.register(Plane, PlaneArtist) -BaseArtist.register(Point, PointArtist) -BaseArtist.register(Polygon, PolygonArtist) -BaseArtist.register(Polyline, PolylineArtist) -BaseArtist.register(Vector, VectorArtist) +ShapeArtist.default_color = (255, 255, 255) + +MeshArtist.default_color = (0, 0, 0) +MeshArtist.default_vertexcolor = (255, 255, 255) +MeshArtist.default_edgecolor = (0, 0, 0) +MeshArtist.default_facecolor = (255, 255, 255) + +NetworkArtist.default_nodecolor = (255, 255, 255) +NetworkArtist.default_edgecolor = (0, 0, 0) + +VolMeshArtist.default_color = (0, 0, 0) +VolMeshArtist.default_vertexcolor = (255, 255, 255) +VolMeshArtist.default_edgecolor = (0, 0, 0) +VolMeshArtist.default_facecolor = (255, 255, 255) +VolMeshArtist.default_cellcolor = (255, 0, 0) + + +def verify_rhino_context(): + try: + import Rhino + import scriptcontext as sc + + return isinstance(sc.doc, Rhino.RhinoDoc) + except: # noqa: E722 + return False + + +artists_registered = False + + +@plugin(category='drawing-utils', pluggable_name='clear', requires=['Rhino', verify_rhino_context]) +def clear_rhino(): + compas_rhino.clear() + + +@plugin(category='drawing-utils', pluggable_name='redraw', requires=['Rhino', verify_rhino_context]) +def redraw_rhino(): + compas_rhino.redraw() + + +@plugin(category='factories', pluggable_name='new_artist', requires=['Rhino', verify_rhino_context]) +def new_artist_rhino(cls, *args, **kwargs): + # "lazy registration" seems necessary to avoid item-artist pairs to be overwritten unintentionally + global artists_registered + + if not artists_registered: + RhinoArtist.register(Circle, CircleArtist) + RhinoArtist.register(Frame, FrameArtist) + RhinoArtist.register(Line, LineArtist) + RhinoArtist.register(Plane, PlaneArtist) + RhinoArtist.register(Point, PointArtist) + RhinoArtist.register(Polygon, PolygonArtist) + RhinoArtist.register(Polyline, PolylineArtist) + RhinoArtist.register(Vector, VectorArtist) + RhinoArtist.register(Box, BoxArtist) + RhinoArtist.register(Capsule, CapsuleArtist) + RhinoArtist.register(Cone, ConeArtist) + RhinoArtist.register(Cylinder, CylinderArtist) + RhinoArtist.register(Polyhedron, PolyhedronArtist) + RhinoArtist.register(Sphere, SphereArtist) + RhinoArtist.register(Torus, TorusArtist) + RhinoArtist.register(Mesh, MeshArtist) + RhinoArtist.register(Network, NetworkArtist) + RhinoArtist.register(VolMesh, VolMeshArtist) + RhinoArtist.register(RobotModel, RobotModelArtist) + artists_registered = True + + data = args[0] + + if 'artist_type' in kwargs: + cls = kwargs['artist_type'] + else: + dtype = type(data) + if dtype not in RhinoArtist.ITEM_ARTIST: + raise DataArtistNotRegistered('No Rhino artist is registered for this data type: {}'.format(dtype)) + cls = RhinoArtist.ITEM_ARTIST[dtype] -BaseArtist.register(Box, BoxArtist) -BaseArtist.register(Capsule, CapsuleArtist) -BaseArtist.register(Cone, ConeArtist) -BaseArtist.register(Cylinder, CylinderArtist) -BaseArtist.register(Polyhedron, PolyhedronArtist) -BaseArtist.register(Sphere, SphereArtist) -BaseArtist.register(Torus, TorusArtist) + # TODO: move this to the plugin module and/or to a dedicated function -BaseArtist.register(Mesh, MeshArtist) -BaseArtist.register(Network, NetworkArtist) -BaseArtist.register(VolMesh, VolMeshArtist) + for name, value in inspect.getmembers(cls): + if inspect.ismethod(value): + if hasattr(value, '__isabstractmethod__'): + raise Exception('Abstract method not implemented: {}'.format(value)) -BaseArtist.register(RobotModel, RobotModelArtist) + return super(Artist, cls).__new__(cls) __all__ = [ - 'BaseArtist', - 'PrimitiveArtist', - 'ShapeArtist', + 'RhinoArtist', 'CircleArtist', 'FrameArtist', 'LineArtist', diff --git a/src/compas_rhino/artists/_artist.py b/src/compas_rhino/artists/_artist.py deleted file mode 100644 index 1f445292b07a..000000000000 --- a/src/compas_rhino/artists/_artist.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import compas_rhino - - -__all__ = ["BaseArtist"] - - -_ITEM_ARTIST = {} - - -class BaseArtist(object): - """Base class for all Rhino artists. - - Attributes - ---------- - guids : list - A list of the GUID of the Rhino objects created by the artist. - - """ - - def __init__(self): - self._guids = [] - - @staticmethod - def register(item_type, artist_type): - _ITEM_ARTIST[item_type] = artist_type - - @staticmethod - def build(item, **kwargs): - """Build an artist corresponding to the item type. - - Parameters - ---------- - kwargs : dict, optional - The keyword arguments (kwargs) collected in a dict. - For relevant options, see the parameter lists of the matching artist type. - - Returns - ------- - :class:`compas_rhino.artists.BaseArtist` - An artist of the type matching the provided item according to an item-artist map. - The map is created by registering item-artist type pairs using ``~BaseArtist.register``. - """ - artist_type = _ITEM_ARTIST[type(item)] - artist = artist_type(item, **kwargs) - return artist - - def draw(self): - raise NotImplementedError - - def redraw(self): - compas_rhino.rs.EnableRedraw(True) - compas_rhino.rs.Redraw() - - def clear(self): - if not self._guids: - return - compas_rhino.delete_objects(self._guids) - self._guids = [] diff --git a/src/compas_rhino/artists/_primitiveartist.py b/src/compas_rhino/artists/_primitiveartist.py deleted file mode 100644 index 7f301a61193f..000000000000 --- a/src/compas_rhino/artists/_primitiveartist.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import compas_rhino -from compas_rhino.artists._artist import BaseArtist - - -__all__ = ["PrimitiveArtist"] - - -class PrimitiveArtist(BaseArtist): - """Base class for artists for geometry primitives. - - Parameters - ---------- - primitive: :class:`compas.geometry.Primitive` - The geometry of the primitive. - color : 3-tuple, optional - The RGB components of the base color of the primitive. - layer : str, optional - The layer in which the primitive should be contained. - - Attributes - ---------- - primitive: :class:`compas.geometry.Primitive` - The geometry of the primitive. - name : str - The name of the primitive. - color : tuple - The RGB components of the base color of the primitive. - layer : str - The layer in which the primitive should be contained. - - """ - - def __init__(self, primitive, color=None, layer=None): - super(PrimitiveArtist, self).__init__() - self.primitive = primitive - self.color = color - self.layer = layer - - @property - def name(self): - """str : Reference to the name of the primitive.""" - return self.primitive.name - - @name.setter - def name(self, name): - self.primitive.name = name - - def clear_layer(self): - """Clear the layer containing the artist.""" - if self.layer: - compas_rhino.clear_layer(self.layer) diff --git a/src/compas_rhino/artists/_shapeartist.py b/src/compas_rhino/artists/_shapeartist.py deleted file mode 100644 index d8dc19f74e15..000000000000 --- a/src/compas_rhino/artists/_shapeartist.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import compas_rhino -from compas.datastructures import Mesh -from compas_rhino.artists._artist import BaseArtist - -__all__ = ['ShapeArtist'] - - -class ShapeArtist(BaseArtist): - """Base class for artists for geometric shapes. - - Parameters - ---------- - shape: :class:`compas.geometry.Shape` - The geometry of the shape. - color : 3-tuple, optional - The RGB components of the base color of the shape. - layer : str, optional - The layer in which the shape should be contained. - - Attributes - ---------- - shape: :class:`compas.geometry.Shape` - The geometry of the shape. - name : str - The name of the shape. - color : tuple - The RGB components of the base color of the shape. - layer : str - The layer in which the shape should be contained. - - """ - - def __init__(self, shape, color=None, layer=None): - super(ShapeArtist, self).__init__() - self._shape = None - self._mesh = None - self.shape = shape - self.color = color - self.layer = layer - - @property - def shape(self): - """:class:`compas.geometry.Shape` : The geometry of the shape.""" - return self._shape - - @shape.setter - def shape(self, shape): - self._shape = shape - self._mesh = Mesh.from_shape(shape) - - @property - def name(self): - """str : Reference to the name of the shape.""" - return self.shape.name - - @name.setter - def name(self, name): - self.shape.name = name - - def clear_layer(self): - """Clear the main layer of the artist.""" - if self.layer: - compas_rhino.clear_layer(self.layer) diff --git a/src/compas_rhino/artists/artist.py b/src/compas_rhino/artists/artist.py new file mode 100644 index 000000000000..c1512559d223 --- /dev/null +++ b/src/compas_rhino/artists/artist.py @@ -0,0 +1,19 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import compas_rhino +from compas.artists import Artist + + +class RhinoArtist(Artist): + """Base class for all Rhino artists. + """ + + def __init__(self, layer=None, **kwargs): + super(RhinoArtist, self).__init__(**kwargs) + self.layer = layer + + def clear_layer(self): + if self.layer: + compas_rhino.clear_layer(self.layer) diff --git a/src/compas_rhino/artists/boxartist.py b/src/compas_rhino/artists/boxartist.py index 2b35035cb9aa..c23656b87dc5 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -3,80 +3,44 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from compas.artists import ShapeArtist +from .artist import RhinoArtist -class BoxArtist(ShapeArtist): +class BoxArtist(RhinoArtist, ShapeArtist): """Artist for drawing box shapes. Parameters ---------- - shape : :class:`compas.geometry.Box` + box : :class:`compas.geometry.Box` A COMPAS box. - - Notes - ----- - See :class:`compas_rhino.artists.ShapeArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Box - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import BoxArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Box.from_width_height_depth(0.3, 0.3, 0.3) - - compas_rhino.clear_layer("Test::BoxArtist") - - for point in pcl.points: - box = tpl.copy() - box.frame.point = point - artist = BoxArtist(box, color=i_to_rgb(random.random()), layer="Test::BoxArtist") - artist.draw() + layer : str, optional + The layer that should contain the drawing. """ - def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + def __init__(self, box, layer=None, **kwargs): + super(BoxArtist, self).__init__(shape=box, layer=layer, **kwargs) + + def draw(self, color=None): """Draw the box associated with the artist. Parameters ---------- - show_vertices : bool, optional - Default is ``False``. - show_edges : bool, optional - Default is ``False``. - show_faces : bool, optional - Default is ``True``. - join_faces : bool, optional - Default is ``True``. + color : tuple of float, optional + The RGB color of the box. Returns ------- list The GUIDs of the objects created in Rhino. """ + color = color or self.color vertices = [list(vertex) for vertex in self.shape.vertices] - guids = [] - if show_vertices: - points = [{'pos': point, 'color': self.color, 'name': self.name} for point in vertices] - guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - if show_edges: - edges = self.shape.edges - lines = [{'start': vertices[i], 'end': vertices[j], 'color': self.color, 'name': self.name} for i, j in edges] - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - if show_faces: - faces = self.shape.faces - if join_faces: - guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.name, color=self.color, disjoint=True) - guids.append(guid) - else: - polygons = [{'points': [vertices[index] for index in face], 'color': self.color, 'name': self.name} for face in faces] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - self._guids = guids - return guids + faces = self.shape.faces + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] diff --git a/src/compas_rhino/artists/capsuleartist.py b/src/compas_rhino/artists/capsuleartist.py index 5ae44cb0ba06..09ecbbf7fce3 100644 --- a/src/compas_rhino/artists/capsuleartist.py +++ b/src/compas_rhino/artists/capsuleartist.py @@ -2,95 +2,53 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from compas.artists import ShapeArtist +from .artist import RhinoArtist -class CapsuleArtist(ShapeArtist): +class CapsuleArtist(RhinoArtist, ShapeArtist): """Artist for drawing capsule shapes. Parameters ---------- - shape : :class:`compas.geometry.Capsule` + capsule : :class:`compas.geometry.Capsule` A COMPAS capsule. - - Notes - ----- - See :class:`compas_rhino.artists.ShapeArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Capsule - from compas.geometry import Translation - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import CapsuleArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Capsule([[0, 0, 0], [0.8, 0, 0]], 0.15) - - compas_rhino.clear_layer("Test::CapsuleArtist") - - for point in pcl.points: - capsule = tpl.transformed(Translation.from_vector(point)) - artist = CapsuleArtist(capsule, color=i_to_rgb(random.random()), layer="Test::CapsuleArtist") - artist.draw() - + layer : str, optional + The layer that should contain the drawing. """ - def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + def __init__(self, capsule, layer=None, **kwargs): + super(CapsuleArtist, self).__init__(shape=capsule, layer=layer, **kwargs) + + def draw(self, color=None, u=None, v=None): """Draw the capsule associated with the artist. Parameters ---------- + color : tuple of float, optional + The RGB color of the capsule. u : int, optional Number of faces in the "u" direction. - Default is ``10``. + Default is ``~CapsuleArtist.u``. v : int, optional Number of faces in the "v" direction. - Default is ``10``. - show_vertices : bool, optional - Default is ``False``. - show_edges : bool, optional - Default is ``False``. - show_faces : bool, optional - Default is ``True``. - join_faces : bool, optional - Default is ``True``. + Default is ``~CapsuleArtist.v``. Returns ------- list The GUIDs of the objects created in Rhino. """ + color = color or self.color + u = u or self.u + v = v or self.v vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) vertices = [list(vertex) for vertex in vertices] - guids = [] - if show_vertices: - points = [{'pos': point, 'color': self.color} for point in vertices] - guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - if show_edges: - lines = [] - seen = set() - for face in faces: - for u, v in pairwise(face + face[:1]): - if (u, v) not in seen: - seen.add((u, v)) - seen.add((v, u)) - lines.append({'start': vertices[u], 'end': vertices[v], 'color': self.color}) - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - if show_faces: - if join_faces: - guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.name, color=self.color, disjoint=True) - guids.append(guid) - else: - polygons = [{'points': [vertices[index] for index in face], 'color': self.color} for face in faces] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - self._guids = guids - return guids + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] diff --git a/src/compas_rhino/artists/circleartist.py b/src/compas_rhino/artists/circleartist.py index 8076f738e0bc..e35ed0e9b60d 100644 --- a/src/compas_rhino/artists/circleartist.py +++ b/src/compas_rhino/artists/circleartist.py @@ -4,49 +4,24 @@ import compas_rhino from compas.geometry import add_vectors -from compas_rhino.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import RhinoArtist -__all__ = ['CircleArtist'] - - -class CircleArtist(PrimitiveArtist): +class CircleArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing circles. Parameters ---------- - primitive : :class:`compas.geometry.Circle` + circle : :class:`compas.geometry.Circle` A COMPAS circle. - - Notes - ----- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Circle - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import CircleArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Circle([[0, 0, 0], [0, -1, 0]], 0.7) - - compas_rhino.clear_layer("Test::CircleArtist") - - for point in pcl.points: - circle = tpl.copy() - circle.plane.point = point - artist = CircleArtist(circle, color=i_to_rgb(random.random()), layer="Test::CircleArtist") - artist.draw() - + layer : str, optional + The layer that should contain the drawing. """ + def __init__(self, circle, layer=None, **kwargs): + super(CircleArtist, self).__init__(primitive=circle, layer=layer, **kwargs) + def draw(self, show_point=False, show_normal=False): """Draw the circle. @@ -68,12 +43,11 @@ def draw(self, show_point=False, show_normal=False): radius = self.primitive.radius guids = [] if show_point: - points = [{'pos': point, 'color': self.color, 'name': self.name}] + points = [{'pos': point, 'color': self.color, 'name': self.primitive.name}] guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) if show_normal: - lines = [{'start': point, 'end': add_vectors(point, normal), 'arrow': 'end', 'color': self.color, 'name': self.name}] + lines = [{'start': point, 'end': add_vectors(point, normal), 'arrow': 'end', 'color': self.color, 'name': self.primitive.name}] guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - circles = [{'plane': plane, 'radius': radius, 'color': self.color, 'name': self.name}] + circles = [{'plane': plane, 'radius': radius, 'color': self.color, 'name': self.primitive.name}] guids += compas_rhino.draw_circles(circles, layer=self.layer, clear=False, redraw=False) - self._guids = guids return guids diff --git a/src/compas_rhino/artists/coneartist.py b/src/compas_rhino/artists/coneartist.py index 797a06e776a1..288b230e325c 100644 --- a/src/compas_rhino/artists/coneartist.py +++ b/src/compas_rhino/artists/coneartist.py @@ -2,94 +2,50 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from compas.artists import ShapeArtist +from .artist import RhinoArtist -class ConeArtist(ShapeArtist): +class ConeArtist(RhinoArtist, ShapeArtist): """Artist for drawing cone shapes. Parameters ---------- shape : :class:`compas.geometry.Cone` A COMPAS cone. - - Notes - ----- - See :class:`compas_rhino.artists.ShapeArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Cone - from compas.geometry import Translation - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import ConeArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 200) - tpl = Cone([[[0, 0, 0], [0, 0, 1]], 0.2], 0.8) - - vertices, faces = tpl.to_vertices_and_faces(4) - - compas_rhino.clear_layer("Test::ConeArtist") - - for point in pcl.points[:len(pcl) // 2]: - cone = tpl.transformed(Translation.from_vector(point)) - artist = ConeArtist(cone, color=i_to_rgb(random.random()), layer="Test::ConeArtist") - artist.draw(u=16) + layer : str, optional + The layer that should contain the drawing. """ - def draw(self, u=10, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + def __init__(self, cone, layer=None, **kwargs): + super(ConeArtist, self).__init__(shape=cone, layer=layer, **kwargs) + + def draw(self, color=None, u=None): """Draw the cone associated with the artist. Parameters ---------- + color : tuple of float, optional + The RGB color of the cone. u : int, optional Number of faces in the "u" direction. - Default is ``10``. - show_vertices : bool, optional - Default is ``False``. - show_edges : bool, optional - Default is ``False``. - show_faces : bool, optional - Default is ``True``. - join_faces : bool, optional - Default is ``True``. + Default is ``~ConeArtist.u``. Returns ------- list The GUIDs of the objects created in Rhino. """ + color = color or self.color + u = u or self.u vertices, faces = self.shape.to_vertices_and_faces(u=u) vertices = [list(vertex) for vertex in vertices] - guids = [] - if show_vertices: - points = [{'pos': point, 'color': self.color} for point in vertices] - guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - if show_edges: - lines = [] - seen = set() - for face in faces: - for u, v in pairwise(face + face[:1]): - if (u, v) not in seen: - seen.add((u, v)) - seen.add((v, u)) - lines.append({'start': vertices[u], 'end': vertices[v], 'color': self.color}) - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - if show_faces: - if join_faces: - guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.name, color=self.color, disjoint=True) - guids.append(guid) - else: - polygons = [{'points': [vertices[index] for index in face], 'color': self.color} for face in faces] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - self._guids = guids - return guids + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] diff --git a/src/compas_rhino/artists/cylinderartist.py b/src/compas_rhino/artists/cylinderartist.py index 06132f49be92..a893b7df37c1 100644 --- a/src/compas_rhino/artists/cylinderartist.py +++ b/src/compas_rhino/artists/cylinderartist.py @@ -2,92 +2,49 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from compas.artists import ShapeArtist +from .artist import RhinoArtist -class CylinderArtist(ShapeArtist): +class CylinderArtist(RhinoArtist, ShapeArtist): """Artist for drawing cylinder shapes. Parameters ---------- - shape : :class:`compas.geometry.Cylinder` + cylinder : :class:`compas.geometry.Cylinder` A COMPAS cylinder. - - Notes - ----- - See :class:`compas_rhino.artists.ShapeArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Cylinder - from compas.geometry import Translation - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import CylinderArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 200) - tpl = Cylinder([[[0, 0, 0], [0, 0, 1]], 0.1], 1.0) - - compas_rhino.clear_layer("Test::CylinderArtist") - - for point in pcl.points[:len(pcl) // 2]: - cylinder = tpl.transformed(Translation.from_vector(point)) - artist = CylinderArtist(cylinder, color=i_to_rgb(random.random()), layer="Test::CylinderArtist") - artist.draw() - + layer : str, optional + The layer that should contain the drawing. """ - def draw(self, u=10, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + def __init__(self, cylinder, layer=None, **kwargs): + super(CylinderArtist, self).__init__(shape=cylinder, layer=layer, **kwargs) + + def draw(self, color=None, u=None): """Draw the cylinder associated with the artist. Parameters ---------- + color : tuple of float, optional + The RGB color of the cylinder. u : int, optional Number of faces in the "u" direction. - Default is ``10``. - show_vertices : bool, optional - Default is ``False``. - show_edges : bool, optional - Default is ``False``. - show_faces : bool, optional - Default is ``True``. - join_faces : bool, optional - Default is ``True``. + Default is ``~CylinderArtist.u``. Returns ------- list The GUIDs of the objects created in Rhino. """ + color = color or self.color + u = u or self.u vertices, faces = self.shape.to_vertices_and_faces(u=u) vertices = [list(vertex) for vertex in vertices] - guids = [] - if show_vertices: - points = [{'pos': point, 'color': self.color} for point in vertices] - guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - if show_edges: - lines = [] - seen = set() - for face in faces: - for u, v in pairwise(face + face[:1]): - if (u, v) not in seen: - seen.add((u, v)) - seen.add((v, u)) - lines.append({'start': vertices[u], 'end': vertices[v], 'color': self.color}) - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - if show_faces: - if join_faces: - guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.name, color=self.color, disjoint=True) - guids.append(guid) - else: - polygons = [{'points': [vertices[index] for index in face], 'color': self.color} for face in faces] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - self._guids = guids - return guids + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] diff --git a/src/compas_rhino/artists/frameartist.py b/src/compas_rhino/artists/frameartist.py index 2cc5481f1752..c3c14ef7ad00 100644 --- a/src/compas_rhino/artists/frameartist.py +++ b/src/compas_rhino/artists/frameartist.py @@ -3,13 +3,11 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import RhinoArtist -__all__ = ['FrameArtist'] - - -class FrameArtist(PrimitiveArtist): +class FrameArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing frames. Parameters @@ -18,10 +16,8 @@ class FrameArtist(PrimitiveArtist): A COMPAS frame. scale: float, optional Scale factor that controls the length of the axes. - - Notes - ----- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. + layer : str, optional + The layer that should contain the drawing. Attributes ---------- @@ -36,32 +32,10 @@ class FrameArtist(PrimitiveArtist): Default is ``(0, 255, 0)``. color_zaxis : tuple of 3 int between 0 and 255 Default is ``(0, 0, 255)``. - - Examples - -------- - .. code-block:: python - - from compas.geometry import Pointcloud - from compas.geometry import Frame - - import compas_rhino - from compas_rhino.artists import FrameArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]) - - compas_rhino.clear_layer("Test::FrameArtist") - - for point in pcl.points: - frame = tpl.copy() - frame.point = point - artist = FrameArtist(frame, layer="Test::FrameArtist") - artist.draw() - """ - def __init__(self, frame, layer=None, scale=1.0): - super(FrameArtist, self).__init__(frame, layer=layer) + def __init__(self, frame, layer=None, scale=1.0, **kwargs): + super(FrameArtist, self).__init__(primitive=frame, layer=layer, **kwargs) self.scale = scale or 1.0 self.color_origin = (0, 0, 0) self.color_xaxis = (255, 0, 0) @@ -89,5 +63,4 @@ def draw(self): {'start': origin, 'end': Z, 'color': self.color_zaxis, 'arrow': 'end'}] guids = compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - self._guids = guids return guids diff --git a/src/compas_rhino/artists/lineartist.py b/src/compas_rhino/artists/lineartist.py index 87480e1a4966..2481d814a94e 100644 --- a/src/compas_rhino/artists/lineartist.py +++ b/src/compas_rhino/artists/lineartist.py @@ -3,48 +3,24 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import RhinoArtist -__all__ = ['LineArtist'] - - -class LineArtist(PrimitiveArtist): +class LineArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing lines. Parameters ---------- - primitive : :class:`compas.geometry.Line` + line : :class:`compas.geometry.Line` A COMPAS line. - - Notes - ----- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Vector - from compas.geometry import Line - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import LineArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - - compas_rhino.clear_layer("Test::LineArtist") - - for point in pcl.points: - line = Line(point, point + Vector(1, 0, 0)) - artist = LineArtist(line, color=i_to_rgb(random.random()), layer="Test::LineArtist") - artist.draw() - + layer : str, optional + The layer that should contain the drawing. """ + def __init__(self, line, layer=None, **kwargs): + super(LineArtist, self).__init__(primitive=line, layer=layer, **kwargs) + def draw(self, show_points=False): """Draw the line. @@ -65,11 +41,10 @@ def draw(self, show_points=False): guids = [] if show_points: points = [ - {'pos': start, 'color': self.color, 'name': self.name}, - {'pos': end, 'color': self.color, 'name': self.name} + {'pos': start, 'color': self.color, 'name': self.primitive.name}, + {'pos': end, 'color': self.color, 'name': self.primitive.name} ] guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - lines = [{'start': start, 'end': end, 'color': self.color, 'name': self.name}] + lines = [{'start': start, 'end': end, 'color': self.color, 'name': self.primitive.name}] guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - self._guids = guids return guids diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index e309efd4b311..57adddbe49ec 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -3,9 +3,6 @@ from __future__ import division from functools import partial -import compas_rhino - -from compas_rhino.artists._artist import BaseArtist from compas.utilities import color_to_colordict from compas.utilities import pairwise @@ -14,14 +11,14 @@ from compas.geometry import centroid_polygon from compas.geometry import centroid_points +import compas_rhino +from compas.artists import MeshArtist +from .artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['MeshArtist'] - - -class MeshArtist(BaseArtist): +class MeshArtist(RhinoArtist, MeshArtist): """Artists for drawing mesh data structures. Parameters @@ -30,112 +27,134 @@ class MeshArtist(BaseArtist): A COMPAS mesh. layer : str, optional The name of the layer that will contain the mesh. - - Attributes - ---------- - mesh : :class:`compas.datastructures.Mesh` - The COMPAS mesh associated with the artist. - layer : str - The layer in which the mesh should be contained. - color_vertices : 3-tuple - Default color of the vertices. - color_edges : 3-tuple - Default color of the edges. - color_faces : 3-tuple - Default color of the faces. - - Examples - -------- - .. code-block:: python - - import compas - from compas.datastructures import Mesh - from compas_rhino.artists import MeshArtist - - mesh = Mesh.from_obj(compas.get('faces.obj')) - - artist = MeshArtist(mesh, layer='COMPAS::MeshArtist') - artist.clear_layer() - artist.draw_faces(join_faces=True) - artist.draw_vertices(color={key: '#ff0000' for key in mesh.vertices_on_boundary()}) - artist.draw_edges() - artist.redraw() - """ - def __init__(self, mesh, layer=None): - super(MeshArtist, self).__init__() - self._mesh = None - self._vertex_xyz = None - self.mesh = mesh - self.layer = layer - self.color_vertices = (255, 255, 255) - self.color_edges = (0, 0, 0) - self.color_faces = (0, 0, 0) - - @property - def mesh(self): - return self._mesh - - @mesh.setter - def mesh(self, mesh): - self._mesh = mesh - self._vertex_xyz = None - - @property - def vertex_xyz(self): - """dict: - The view coordinates of the mesh vertices. - The view coordinates default to the actual mesh coordinates. - """ - if not self._vertex_xyz: - return {vertex: self.mesh.vertex_attributes(vertex, 'xyz') for vertex in self.mesh.vertices()} - return self._vertex_xyz - - @vertex_xyz.setter - def vertex_xyz(self, vertex_xyz): - self._vertex_xyz = vertex_xyz + def __init__(self, + mesh, + layer=None, + vertices=None, + edges=None, + faces=None, + vertexcolor=(255, 255, 255), + edgecolor=(0, 0, 0), + facecolor=(221, 221, 221), + show_mesh=False, + show_vertices=True, + show_edges=True, + show_faces=True, + **kwargs): + + super(MeshArtist, self).__init__(mesh=mesh, layer=layer, **kwargs) + + self.vertices = vertices + self.edges = edges + self.faces = faces + self.vertex_color = vertexcolor + self.edge_color = edgecolor + self.face_color = facecolor + self.show_mesh = show_mesh + self.show_vertices = show_vertices + self.show_edges = show_edges + self.show_faces = show_faces # ========================================================================== # clear # ========================================================================== - def clear_by_name(self): - """Clear all objects in the "namespace" of the associated mesh.""" + def clear(self): guids = compas_rhino.get_objects(name="{}.*".format(self.mesh.name)) compas_rhino.delete_objects(guids, purge=True) - def clear_layer(self): - """Clear the main layer of the artist.""" - if self.layer: - compas_rhino.clear_layer(self.layer) + def clear_mesh(self): + guids = compas_rhino.get_objects(name="{}.mesh".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_vertices(self): + guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_edges(self): + guids = compas_rhino.get_objects(name="{}.edge.*".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_faces(self): + guids = compas_rhino.get_objects(name="{}.face.*".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_vertexnormals(self): + guids = compas_rhino.get_objects(name="{}.vertexnormal.*".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_facenormals(self): + guids = compas_rhino.get_objects(name="{}.facenormal.*".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_vertexlabels(self): + guids = compas_rhino.get_objects(name="{}.vertexlabel.*".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_edgelabels(self): + guids = compas_rhino.get_objects(name="{}.edgelabel.*".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_facelabels(self): + guids = compas_rhino.get_objects(name="{}.facelabel.*".format(self.mesh.name)) + compas_rhino.delete_objects(guids, purge=True) # ========================================================================== # draw # ========================================================================== - def draw(self): - """Draw the mesh using the chosen visualisation settings. + def draw(self, vertices=None, edges=None, faces=None, vertexcolor=None, edgecolor=None, facecolor=None, join_faces=False): + """Draw the network using the chosen visualisation settings. + + Parameters + ---------- + vertices : list, optional + A list of vertices to draw. + Default is ``None``, in which case all vertices are drawn. + edges : list, optional + A list of edges to draw. + The default is ``None``, in which case all edges are drawn. + faces : list, optional + A selection of faces to draw. + The default is ``None``, in which case all faces are drawn. + vertexcolor : tuple or dict of tuple, optional + The color specififcation for the vertices. + The default color is the value of ``~MeshArtist.default_vertexcolor``. + edgecolor : tuple or dict of tuple, optional + The color specififcation for the edges. + The default color is the value of ``~MeshArtist.default_edgecolor``. + facecolor : tuple or dict of tuple, optional + The color specififcation for the faces. + The default color is the value of ``~MeshArtist.default_facecolor``. + join_faces : bool, optional + Join the faces into 1 mesh. + Default is ``False``, in which case the faces are drawn as individual meshes. Returns ------- - list - The GUIDs of the created Rhino objects. + None """ - guids = self.draw_vertices() - guids += self.draw_faces() - guids += self.draw_edges() - return guids - - def draw_mesh(self, color=(0, 0, 0), disjoint=False): + self.clear() + if self.show_mesh: + self.draw_mesh() + if self.show_vertices: + self.draw_vertices(vertices=vertices, color=vertexcolor) + if self.show_edges: + self.draw_edges(edges=edges, color=edgecolor) + if self.show_faces: + self.draw_faces(faces=faces, color=facecolor, join_faces=join_faces) + + def draw_mesh(self, color=None, disjoint=False): """Draw the mesh as a consolidated RhinoMesh. Parameters ---------- color : tuple, optional The color of the mesh. - Default is black, ``(0, 0, 0)``. + Default is the value of ``~MeshArtist.default_color``. disjoint : bool, optional Draw the faces of the mesh with disjoint vertices. Default is ``False``. @@ -150,7 +169,8 @@ def draw_mesh(self, color=(0, 0, 0), disjoint=False): The mesh should be a valid Rhino Mesh object, which means it should have only triangular or quadrilateral faces. Faces with more than 4 vertices will be triangulated on-the-fly. """ - vertex_index = self.mesh.key_index() + color = color or self.default_color + vertex_index = self.mesh.vertex_index() vertex_xyz = self.vertex_xyz vertices = [vertex_xyz[vertex] for vertex in self.mesh.vertices()] faces = [[vertex_index[vertex] for vertex in self.mesh.face_vertices(face)] for face in self.mesh.faces()] @@ -166,10 +186,8 @@ def draw_mesh(self, color=(0, 0, 0), disjoint=False): vertices.append(centroid_polygon([vertices[index] for index in face])) for a, b in pairwise(face + face[0:1]): new_faces.append([centroid, a, b, b]) - else: - continue layer = self.layer - name = "{}".format(self.mesh.name) + name = "{}.mesh".format(self.mesh.name) guid = compas_rhino.draw_mesh(vertices, new_faces, layer=layer, name=name, color=color, disjoint=disjoint) return [guid] @@ -178,12 +196,12 @@ def draw_vertices(self, vertices=None, color=None): Parameters ---------- - vertices : list + vertices : list, optional A selection of vertices to draw. Default is ``None``, in which case all vertices are drawn. color : tuple or dict of tuple, optional The color specififcation for the vertices. - The default is white, ``(255, 255, 255)``. + The default is the value of ``~MeshArtist.default_vertexcolor``. Returns ------- @@ -191,16 +209,50 @@ def draw_vertices(self, vertices=None, color=None): The GUIDs of the created Rhino objects. """ - vertices = vertices or list(self.mesh.vertices()) + self.vertex_color = color + vertices = vertices or self.vertices vertex_xyz = self.vertex_xyz - vertex_color = colordict(color, vertices, default=self.color_vertices) points = [] for vertex in vertices: points.append({ 'pos': vertex_xyz[vertex], 'name': "{}.vertex.{}".format(self.mesh.name, vertex), - 'color': vertex_color[vertex]}) - return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) + 'color': self.vertex_color.get(vertex, self.default_vertexcolor) + }) + guids = compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) + return guids + + def draw_edges(self, edges=None, color=None): + """Draw a selection of edges. + + Parameters + ---------- + edges : list, optional + A selection of edges to draw. + The default is ``None``, in which case all edges are drawn. + color : tuple or dict of tuple, optional + The color specififcation for the edges. + The default color is the value of ``~MeshArtist.default_edgecolor``. + + Returns + ------- + list + The GUIDs of the created Rhino objects. + + """ + self.edge_color = color + edges = edges or self.edges + vertex_xyz = self.vertex_xyz + lines = [] + for edge in edges: + lines.append({ + 'start': vertex_xyz[edge[0]], + 'end': vertex_xyz[edge[1]], + 'color': self.edge_color.get(edge, self.default_edgecolor), + 'name': "{}.edge.{}-{}".format(self.mesh.name, *edge) + }) + guids = compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) + return guids def draw_faces(self, faces=None, color=None, join_faces=False): """Draw a selection of faces. @@ -212,7 +264,7 @@ def draw_faces(self, faces=None, color=None, join_faces=False): The default is ``None``, in which case all faces are drawn. color : tuple or dict of tuple, optional The color specififcation for the faces. - The default color is black ``(0, 0, 0)``. + The default color is the value of ``~MeshArtist.default_facecolor``. join_faces : bool, optional Join the faces into 1 mesh. Default is ``False``, in which case the faces are drawn as individual meshes. @@ -223,54 +275,24 @@ def draw_faces(self, faces=None, color=None, join_faces=False): The GUIDs of the created Rhino objects. """ - faces = faces or list(self.mesh.faces()) + self.face_color = color + faces = faces or self.faces vertex_xyz = self.vertex_xyz - face_color = colordict(color, faces, default=self.color_faces) facets = [] for face in faces: facets.append({ 'points': [vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)], 'name': "{}.face.{}".format(self.mesh.name, face), - 'color': face_color[face]}) + 'color': self.face_color.get(face, self.default_facecolor) + }) guids = compas_rhino.draw_faces(facets, layer=self.layer, clear=False, redraw=False) - if not join_faces: - return guids - guid = compas_rhino.rs.JoinMeshes(guids, delete_input=True) - compas_rhino.rs.ObjectLayer(guid, self.layer) - compas_rhino.rs.ObjectName(guid, '{}'.format(self.mesh.name)) - if color: + if join_faces: + guid = compas_rhino.rs.JoinMeshes(guids, delete_input=True) + compas_rhino.rs.ObjectLayer(guid, self.layer) + compas_rhino.rs.ObjectName(guid, '{}.mesh'.format(self.mesh.name)) compas_rhino.rs.ObjectColor(guid, color) - return [guid] - - def draw_edges(self, edges=None, color=None): - """Draw a selection of edges. - - Parameters - ---------- - edges : list, optional - A selection of edges to draw. - The default is ``None``, in which case all edges are drawn. - color : tuple or dict of tuple, optional - The color specififcation for the edges. - The default color is black, ``(0, 0, 0)``. - - Returns - ------- - list - The GUIDs of the created Rhino objects. - - """ - edges = edges or list(self.mesh.edges()) - vertex_xyz = self.vertex_xyz - edge_color = colordict(color, edges, default=self.color_edges) - lines = [] - for edge in edges: - lines.append({ - 'start': vertex_xyz[edge[0]], - 'end': vertex_xyz[edge[1]], - 'color': edge_color[edge], - 'name': "{}.edge.{}-{}".format(self.mesh.name, *edge)}) - return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) + guids = [guid] + return guids # ========================================================================== # draw normals @@ -298,7 +320,7 @@ def draw_vertexnormals(self, vertices=None, color=(0, 255, 0), scale=1.0): """ vertex_xyz = self.vertex_xyz - vertices = vertices or list(self.mesh.vertices()) + vertices = vertices or self.vertices lines = [] for vertex in vertices: a = vertex_xyz[vertex] @@ -309,7 +331,8 @@ def draw_vertexnormals(self, vertices=None, color=(0, 255, 0), scale=1.0): 'end': b, 'color': color, 'name': "{}.vertexnormal.{}".format(self.mesh.name, vertex), - 'arrow': 'end'}) + 'arrow': 'end' + }) return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) def draw_facenormals(self, faces=None, color=(0, 255, 255), scale=1.0): @@ -334,7 +357,7 @@ def draw_facenormals(self, faces=None, color=(0, 255, 255), scale=1.0): """ vertex_xyz = self.vertex_xyz - faces = faces or list(self.mesh.faces()) + faces = faces or self.faces lines = [] for face in faces: a = centroid_points([vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]) @@ -345,7 +368,8 @@ def draw_facenormals(self, faces=None, color=(0, 255, 255), scale=1.0): 'end': b, 'name': "{}.facenormal.{}".format(self.mesh.name, face), 'color': color, - 'arrow': 'end'}) + 'arrow': 'end' + }) return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) # ========================================================================== @@ -371,35 +395,36 @@ def draw_vertexlabels(self, text=None, color=None): """ if not text or text == 'key': - vertex_text = {vertex: str(vertex) for vertex in self.mesh.vertices()} + vertex_text = {vertex: str(vertex) for vertex in self.vertices} elif text == 'index': - vertex_text = {vertex: str(index) for index, vertex in enumerate(self.mesh.vertices())} + vertex_text = {vertex: str(index) for index, vertex in enumerate(self.vertices)} elif isinstance(text, dict): vertex_text = text else: raise NotImplementedError vertex_xyz = self.vertex_xyz - vertex_color = colordict(color, vertex_text.keys(), default=self.color_vertices) + vertex_color = colordict(color, vertex_text.keys(), default=self.default_vertexcolor) labels = [] for vertex in vertex_text: labels.append({ 'pos': vertex_xyz[vertex], 'name': "{}.vertexlabel.{}".format(self.mesh.name, vertex), 'color': vertex_color[vertex], - 'text': vertex_text[vertex]}) + 'text': vertex_text[vertex] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) - def draw_facelabels(self, text=None, color=None): - """Draw labels for a selection of faces. + def draw_edgelabels(self, text=None, color=None): + """Draw labels for a selection of edges. Parameters ---------- text : dict, optional - A dictionary of face labels as face-text pairs. - The default value is ``None``, in which case every face will be labelled with its key. + A dictionary of edge labels as edge-text pairs. + The default value is ``None``, in which case every edge will be labelled with its key. color : tuple or dict of tuple, optional The color specification of the labels. - The default color is the same as the default face color. + The default color is the same as the default color for edges. Returns ------- @@ -407,36 +432,35 @@ def draw_facelabels(self, text=None, color=None): The GUIDs of the created Rhino objects. """ - if not text or text == 'key': - face_text = {face: str(face) for face in self.mesh.faces()} - elif text == 'index': - face_text = {face: str(index) for index, face in enumerate(self.mesh.faces())} + if text is None: + edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.edges} elif isinstance(text, dict): - face_text = text + edge_text = text else: raise NotImplementedError vertex_xyz = self.vertex_xyz - face_color = colordict(color, face_text.keys(), default=self.color_faces) + edge_color = colordict(color, edge_text.keys(), default=self.default_edgecolor) labels = [] - for face in face_text: + for edge in edge_text: labels.append({ - 'pos': centroid_points([vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]), - 'name': "{}.facelabel.{}".format(self.mesh.name, face), - 'color': face_color[face], - 'text': face_text[face]}) + 'pos': centroid_points([vertex_xyz[edge[0]], vertex_xyz[edge[1]]]), + 'name': "{}.edgelabel.{}-{}".format(self.mesh.name, *edge), + 'color': edge_color[edge], + 'text': edge_text[edge] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) - def draw_edgelabels(self, text=None, color=None): - """Draw labels for a selection of edges. + def draw_facelabels(self, text=None, color=None): + """Draw labels for a selection of faces. Parameters ---------- text : dict, optional - A dictionary of edge labels as edge-text pairs. - The default value is ``None``, in which case every edge will be labelled with its key. + A dictionary of face labels as face-text pairs. + The default value is ``None``, in which case every face will be labelled with its key. color : tuple or dict of tuple, optional The color specification of the labels. - The default color is the same as the default color for edges. + The default color is the same as the default face color. Returns ------- @@ -444,19 +468,22 @@ def draw_edgelabels(self, text=None, color=None): The GUIDs of the created Rhino objects. """ - if text is None: - edge_text = {(u, v): "{}-{}".format(u, v) for u, v in self.mesh.edges()} + if not text or text == 'key': + face_text = {face: str(face) for face in self.faces} + elif text == 'index': + face_text = {face: str(index) for index, face in enumerate(self.faces)} elif isinstance(text, dict): - edge_text = text + face_text = text else: raise NotImplementedError vertex_xyz = self.vertex_xyz - edge_color = colordict(color, edge_text.keys(), default=self.color_edges) + face_color = colordict(color, face_text.keys(), default=self.default_facecolor) labels = [] - for edge in edge_text: + for face in face_text: labels.append({ - 'pos': centroid_points([vertex_xyz[edge[0]], vertex_xyz[edge[1]]]), - 'name': "{}.edgelabel.{}-{}".format(self.mesh.name, *edge), - 'color': edge_color[edge], - 'text': edge_text[edge]}) + 'pos': centroid_points([vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]), + 'name': "{}.facelabel.{}".format(self.mesh.name, face), + 'color': face_color[face], + 'text': face_text[face] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) diff --git a/src/compas_rhino/artists/networkartist.py b/src/compas_rhino/artists/networkartist.py index e5ada0b82b35..8714cfb5d5d5 100644 --- a/src/compas_rhino/artists/networkartist.py +++ b/src/compas_rhino/artists/networkartist.py @@ -4,18 +4,17 @@ from functools import partial import compas_rhino -from compas_rhino.artists._artist import BaseArtist + from compas.geometry import centroid_points from compas.utilities import color_to_colordict +from compas.artists import NetworkArtist +from .artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['NetworkArtist'] - - -class NetworkArtist(BaseArtist): +class NetworkArtist(RhinoArtist, NetworkArtist): """Artist for drawing network data structures. Parameters @@ -24,81 +23,94 @@ class NetworkArtist(BaseArtist): A COMPAS network. layer : str, optional The parent layer of the network. - - Attributes - ---------- - network : :class:`compas.datastructures.Network` - The COMPAS network associated with the artist. - layer : str - The layer in which the network should be contained. - color_nodes : 3-tuple - Default color of the nodes. - color_edges : 3-tuple - Default color of the edges. - + nodes : list of int, optional + A list of node identifiers. + Default is ``None``, in which case all nodes are drawn. + edges : list, optional + A list of edge keys (as uv pairs) identifying which edges to draw. + The default is ``None``, in which case all edges are drawn. + nodecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the nodes. + edgecolor : rgb-tuple or dict of rgb-tuples, optional + The color specification for the edges. + show_nodes : bool, optional + show_edges : bool, optional """ - def __init__(self, network, layer=None): - super(NetworkArtist, self).__init__() - self._network = None - self._node_xyz = None - self.network = network - self.layer = layer - self.color_nodes = (255, 255, 255) - self.color_edges = (0, 0, 0) - - @property - def network(self): - return self._network - - @network.setter - def network(self, network): - self._network = network - self._node_xyz = None - - @property - def node_xyz(self): - """dict: - The view coordinates of the network nodes. - The view coordinates default to the actual node coordinates. - """ - if not self._node_xyz: - return {node: self.network.node_attributes(node, 'xyz') for node in self.network.nodes()} - return self._node_xyz - - @node_xyz.setter - def node_xyz(self, node_xyz): - self._node_xyz = node_xyz + def __init__(self, + network, + layer=None, + nodes=None, + edges=None, + nodecolor=None, + edgecolor=None, + show_nodes=True, + show_edges=True, + **kwargs): + + super(NetworkArtist, self).__init__(network=network, layer=layer, **kwargs) + + self.nodes = nodes + self.edges = edges + self.node_color = nodecolor + self.edge_color = edgecolor + self.show_nodes = show_nodes + self.show_edges = show_edges # ========================================================================== # clear # ========================================================================== - def clear_by_name(self): - """Clear all objects in the "namespace" of the associated network.""" + def clear(self): guids = compas_rhino.get_objects(name="{}.*".format(self.network.name)) compas_rhino.delete_objects(guids, purge=True) - def clear_layer(self): - """Clear the main layer of the artist.""" - if self.layer: - compas_rhino.clear_layer(self.layer) + def clear_nodes(self): + guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.network.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_edges(self): + guids = compas_rhino.get_objects(name="{}.edge.*".format(self.network.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_nodelabels(self): + guids = compas_rhino.get_objects(name="{}.nodexlabel.*".format(self.network.name)) + compas_rhino.delete_objects(guids, purge=True) + + def clear_edgelabels(self): + guids = compas_rhino.get_objects(name="{}.edgelabel.*".format(self.network.name)) + compas_rhino.delete_objects(guids, purge=True) # ========================================================================== # draw # ========================================================================== - def draw(self): + def draw(self, nodes=None, edges=None, nodecolor=None, edgecolor=None): """Draw the network using the chosen visualisation settings. + Parameters + ---------- + nodes : list, optional + A list of nodes to draw. + Default is ``None``, in which case all nodes are drawn. + edges : list, optional + A list of edges to draw. + The default is ``None``, in which case all edges are drawn. + nodecolor : tuple or dict of tuple, optional + The color specififcation for the nodes. + The default color is the value of ``~NetworkArtist.default_nodecolor``. + edgecolor : tuple or dict of tuple, optional + The color specififcation for the edges. + The default color is the value of ``~NetworkArtist.default_edgecolor``. + Returns ------- list The GUIDs of the created Rhino objects. - """ - guids = self.draw_nodes() - guids += self.draw_edges() + self.clear() + guids = self.draw_nodes(nodes=nodes, color=nodecolor) + guids += self.draw_edges(edges=edges, color=edgecolor) return guids def draw_nodes(self, nodes=None, color=None): @@ -109,25 +121,25 @@ def draw_nodes(self, nodes=None, color=None): nodes : list, optional A list of nodes to draw. Default is ``None``, in which case all nodes are drawn. - color : 3-tuple or dict of 3-tuples, optional + color : tuple or dict of tuple, optional The color specififcation for the nodes. - The default color is ``(255, 255, 255)``. + The default color is the value of ``~NetworkArtist.default_nodecolor``. Returns ------- list The GUIDs of the created Rhino objects. - """ + self.node_color = color node_xyz = self.node_xyz - nodes = nodes or list(self.network.nodes()) - node_color = colordict(color, nodes, default=self.color_nodes) + nodes = nodes or self.nodes points = [] for node in nodes: points.append({ 'pos': node_xyz[node], 'name': "{}.node.{}".format(self.network.name, node), - 'color': node_color[node]}) + 'color': self.node_color.get(node, self.default_nodecolor) + }) return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) def draw_edges(self, edges=None, color=None): @@ -138,26 +150,26 @@ def draw_edges(self, edges=None, color=None): edges : list, optional A list of edges to draw. The default is ``None``, in which case all edges are drawn. - color : 3-tuple or dict of 3-tuple, optional + color : tuple or dict of tuple, optional The color specififcation for the edges. - The default color is ``(0, 0, 0)``. + The default color is the value of ``~NetworkArtist.default_edgecolor``. Returns ------- list The GUIDs of the created Rhino objects. - """ + self.edge_color = color node_xyz = self.node_xyz - edges = edges or list(self.network.edges()) - edge_color = colordict(color, edges, default=self.color_edges) + edges = edges or self.edges lines = [] for edge in edges: lines.append({ 'start': node_xyz[edge[0]], 'end': node_xyz[edge[1]], - 'color': edge_color[edge], - 'name': "{}.edge.{}-{}".format(self.network.name, *edge)}) + 'color': self.edge_color.get(edge, self.default_edgecolor), + 'name': "{}.edge.{}-{}".format(self.network.name, *edge) + }) return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) # ========================================================================== @@ -180,25 +192,25 @@ def draw_nodelabels(self, text=None, color=None): ------- list The GUIDs of the created Rhino objects. - """ if not text or text == 'key': - node_text = {node: str(node) for node in self.network.nodes()} + node_text = {node: str(node) for node in self.nodes} elif text == 'index': - node_text = {node: str(index) for index, node in enumerate(self.network.nodes())} + node_text = {node: str(index) for index, node in enumerate(self.nodes)} elif isinstance(text, dict): node_text = text else: raise NotImplementedError node_xyz = self.node_xyz - node_color = colordict(color, node_text.keys(), default=self.color_nodes) + node_color = colordict(color, node_text.keys(), default=self.default_nodecolor) labels = [] for node in node_text: labels.append({ 'pos': node_xyz[node], 'name': "{}.nodelabel.{}".format(self.network.name, node), 'color': node_color[node], - 'text': node_text[node]}) + 'text': node_text[node] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) def draw_edgelabels(self, text=None, color=None): @@ -217,21 +229,21 @@ def draw_edgelabels(self, text=None, color=None): ------- list The GUIDs of the created Rhino objects. - """ if text is None: - edge_text = {edge: "{}-{}".format(*edge) for edge in self.network.edges()} + edge_text = {edge: "{}-{}".format(*edge) for edge in self.edges} elif isinstance(text, dict): edge_text = text else: raise NotImplementedError node_xyz = self.node_xyz - edge_color = colordict(color, edge_text.keys(), default=self.color_edges) + edge_color = colordict(color, edge_text.keys(), default=self.default_edgecolor) labels = [] for edge in edge_text: labels.append({ 'pos': centroid_points([node_xyz[edge[0]], node_xyz[edge[1]]]), 'name': "{}.edgelabel.{}-{}".format(self.network.name, *edge), 'color': edge_color[edge], - 'text': edge_text[edge]}) + 'text': edge_text[edge] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) diff --git a/src/compas_rhino/artists/planeartist.py b/src/compas_rhino/artists/planeartist.py index ea66aa8b7669..47818ba69384 100644 --- a/src/compas_rhino/artists/planeartist.py +++ b/src/compas_rhino/artists/planeartist.py @@ -2,27 +2,24 @@ from __future__ import absolute_import from __future__ import division -# import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import RhinoArtist -__all__ = ['PlaneArtist'] - - -class PlaneArtist(PrimitiveArtist): +class PlaneArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing planes. Parameters ---------- - primitive : :class:`compas.geometry.Plane` + plane : :class:`compas.geometry.Plane` A COMPAS plane. - - Notes - ----- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - + layer : str, optional + The layer that should contain the drawing. """ + def __init__(self, plane, layer=None, **kwargs): + super(PlaneArtist, self).__init__(primitive=plane, layer=layer, **kwargs) + def draw(self): """Draw the plane. diff --git a/src/compas_rhino/artists/pointartist.py b/src/compas_rhino/artists/pointartist.py index d1d32f128190..483ea9c4afcf 100644 --- a/src/compas_rhino/artists/pointartist.py +++ b/src/compas_rhino/artists/pointartist.py @@ -3,45 +3,24 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import RhinoArtist -__all__ = ['PointArtist'] - - -class PointArtist(PrimitiveArtist): +class PointArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing points. Parameters ---------- - primitive : :class:`compas.geometry.Point` + point : :class:`compas.geometry.Point` A COMPAS point. - - Notes - ----- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import PointArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - - compas_rhino.clear_layer("Test::PointArtist") - - for point in pcl.points: - artist = PointArtist(point, color=i_to_rgb(random.random()), layer="Test::PointArtist") - artist.draw() - + layer : str, optional + The layer that should contain the drawing. """ + def __init__(self, point, layer=None, **kwargs): + super(PointArtist, self).__init__(primitive=point, layer=layer, **kwargs) + def draw(self): """Draw the point. @@ -51,7 +30,6 @@ def draw(self): The GUIDs of the created Rhino objects. """ - points = [{'pos': list(self.primitive), 'color': self.color, 'name': self.name}] + points = [{'pos': list(self.primitive), 'color': self.color, 'name': self.primitive.name}] guids = compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - self._guids = guids return guids diff --git a/src/compas_rhino/artists/polygonartist.py b/src/compas_rhino/artists/polygonartist.py index 6d8e353cb5bc..42081bc458c5 100644 --- a/src/compas_rhino/artists/polygonartist.py +++ b/src/compas_rhino/artists/polygonartist.py @@ -3,49 +3,24 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import RhinoArtist -__all__ = ['PolygonArtist'] - - -class PolygonArtist(PrimitiveArtist): +class PolygonArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing polygons. Parameters ---------- - primitive : :class:`compas.geometry.Polygon` + polygon : :class:`compas.geometry.Polygon` A COMPAS polygon. - - Notes - ----- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Polygon - from compas.geometry import Translation - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import PolygonArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Polygon.from_sides_and_radius_xy(7, 0.8) - - compas_rhino.clear_layer("Test::PolygonArtist") - - for point in pcl.points: - polygon = tpl.transformed(Translation.from_vector(point)) - artist = PolygonArtist(polygon, color=i_to_rgb(random.random()), layer="Test::PolygonArtist") - artist.draw() - + layer : str, optional + The name of the layer that will contain the mesh. """ + def __init__(self, polygon, layer=None, **kwargs): + super(PolygonArtist, self).__init__(primitive=polygon, layer=layer, **kwargs) + def draw(self, show_points=False, show_edges=False, show_face=True): """Draw the polygon. @@ -66,13 +41,12 @@ def draw(self, show_points=False, show_edges=False, show_face=True): _points = map(list, self.primitive.points) guids = [] if show_points: - points = [{'pos': point, 'color': self.color, 'name': self.name} for point in _points] + points = [{'pos': point, 'color': self.color, 'name': self.primitive.name} for point in _points] guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) if show_edges: - lines = [{'start': list(a), 'end': list(b), 'color': self.color, 'name': self.name} for a, b in self.primitive.lines] + lines = [{'start': list(a), 'end': list(b), 'color': self.color, 'name': self.primitive.name} for a, b in self.primitive.lines] guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) if show_face: - polygons = [{'points': _points, 'color': self.color, 'name': self.name}] + polygons = [{'points': _points, 'color': self.color, 'name': self.primitive.name}] guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - self._guids = guids return guids diff --git a/src/compas_rhino/artists/polyhedronartist.py b/src/compas_rhino/artists/polyhedronartist.py index b6b269a3f5a6..e185318c5987 100644 --- a/src/compas_rhino/artists/polyhedronartist.py +++ b/src/compas_rhino/artists/polyhedronartist.py @@ -3,80 +3,44 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from compas.artists import ShapeArtist +from .artist import RhinoArtist -class PolyhedronArtist(ShapeArtist): +class PolyhedronArtist(RhinoArtist, ShapeArtist): """Artist for drawing polyhedron shapes. Parameters ---------- - shape : :class:`compas.geometry.Polyhedron` + polyhedron : :class:`compas.geometry.Polyhedron` A COMPAS polyhedron. - - Notes - ----- - See :class:`compas_rhino.artists.ShapeArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Polyhedron - from compas.geometry import Translation - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import PolyhedronArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Polyhedron.from_platonicsolid(12) - - compas_rhino.clear_layer("Test::PolyhedronArtist") - - for point in pcl.points: - polyhedron = tpl.transformed(Translation.from_vector(point)) - artist = PolyhedronArtist(polyhedron, color=i_to_rgb(random.random()), layer="Test::PolyhedronArtist") - artist.draw() + layer : str, optional + The layer that should contain the drawing. """ - def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + def __init__(self, polyhedron, layer=None, **kwargs): + super(PolyhedronArtist, self).__init__(shape=polyhedron, layer=layer, **kwargs) + + def draw(self, color=None): """Draw the polyhedron associated with the artist. Parameters ---------- - show_vertices : bool, optional - Default is ``False``. - show_edges : bool, optional - Default is ``False``. - show_faces : bool, optional - Default is ``True``. - join_faces : bool, optional - Default is ``True``. + color : tuple of float, optional + The RGB color of the polyhedron. Returns ------- list The GUIDs of the objects created in Rhino. """ + color = color or self.color vertices = [list(vertex) for vertex in self.shape.vertices] - guids = [] - if show_vertices: - points = [{'pos': point, 'color': self.color, 'name': str(index)} for index, point in enumerate(vertices)] - guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - if show_edges: - edges = self.shape.edges - lines = [{'start': vertices[i], 'end': vertices[j], 'color': self.color} for i, j in edges] - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - if show_faces: - faces = self.shape.faces - if join_faces: - guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.name, color=self.color, disjoint=True) - guids.append(guid) - else: - polygons = [{'points': [vertices[index] for index in face], 'color': self.color} for face in faces] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - self._guids = guids - return guids + faces = self.shape.faces + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] diff --git a/src/compas_rhino/artists/polylineartist.py b/src/compas_rhino/artists/polylineartist.py index ee989993e988..830f21c1678a 100644 --- a/src/compas_rhino/artists/polylineartist.py +++ b/src/compas_rhino/artists/polylineartist.py @@ -3,49 +3,22 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import RhinoArtist -__all__ = ['PolylineArtist'] - - -class PolylineArtist(PrimitiveArtist): +class PolylineArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing polylines. Parameters ---------- - primitive : :class:`compas.geometry.Polyline` + polyline : :class:`compas.geometry.Polyline` A COMPAS polyline. - - Notes - ----- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Polyline - from compas.geometry import Translation - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import PolylineArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Polyline(Polygon.from_sides_and_radius_xy(7, 0.8).points) - - compas_rhino.clear_layer("Test::PolylineArtist") - - for point in pcl.points: - polyline = tpl.transformed(Translation.from_vector(point)) - artist = PolylineArtist(polygon, color=i_to_rgb(random.random()), layer="Test::PolylineArtist") - artist.draw() - """ + def __init__(self, polyline, layer=None, **kwargs): + super(PolylineArtist, self).__init__(primitive=polyline, layer=layer, **kwargs) + def draw(self, show_points=False): """Draw the polyline. @@ -62,9 +35,8 @@ def draw(self, show_points=False): _points = map(list, self.primitive.points) guids = [] if show_points: - points = [{'pos': point, 'color': self.color, 'name': self.name} for point in _points] + points = [{'pos': point, 'color': self.color, 'name': self.primitive.name} for point in _points] guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - polylines = [{'points': _points, 'color': self.color, 'name': self.name}] + polylines = [{'points': _points, 'color': self.color, 'name': self.primitive.name}] guids = compas_rhino.draw_polylines(polylines, layer=self.layer, clear=False, redraw=False) - self._guids = guids return guids diff --git a/src/compas_rhino/artists/robotmodelartist.py b/src/compas_rhino/artists/robotmodelartist.py index b7d50024a2e7..48267ae92ee8 100644 --- a/src/compas_rhino/artists/robotmodelartist.py +++ b/src/compas_rhino/artists/robotmodelartist.py @@ -13,19 +13,15 @@ from compas.geometry import centroid_polygon from compas.utilities import pairwise -from compas.robots.base_artist import BaseRobotModelArtist +from compas.artists import RobotModelArtist import compas_rhino -from compas_rhino.artists import BaseArtist +from compas_rhino.artists import RhinoArtist from compas_rhino.geometry.transformations import xform_from_transformation -__all__ = [ - 'RobotModelArtist', -] - -class RobotModelArtist(BaseRobotModelArtist, BaseArtist): - """Visualizer for robots inside a Rhino environment. +class RobotModelArtist(RhinoArtist, RobotModelArtist): + """Artist for drawing robot models. Parameters ---------- @@ -35,9 +31,8 @@ class RobotModelArtist(BaseRobotModelArtist, BaseArtist): The name of the layer that will contain the robot meshes. """ - def __init__(self, model, layer=None): - super(RobotModelArtist, self).__init__(model) - self.layer = layer + def __init__(self, model, layer=None, **kwargs): + super(RobotModelArtist, self).__init__(model=model, layer=layer, **kwargs) def transform(self, native_mesh, transformation): T = xform_from_transformation(transformation) diff --git a/src/compas_rhino/artists/sphereartist.py b/src/compas_rhino/artists/sphereartist.py index 9068cf30796a..dc85aa0298b5 100644 --- a/src/compas_rhino/artists/sphereartist.py +++ b/src/compas_rhino/artists/sphereartist.py @@ -2,95 +2,53 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from compas.artists import ShapeArtist +from .artist import RhinoArtist -class SphereArtist(ShapeArtist): +class SphereArtist(RhinoArtist, ShapeArtist): """Artist for drawing sphere shapes. Parameters ---------- - shape : :class:`compas.geometry.Sphere` + sphere : :class:`compas.geometry.Sphere` A COMPAS sphere. - - Notes - ----- - See :class:`compas_rhino.artists.ShapeArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Sphere - from compas.geometry import Translation - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import SphereArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Sphere([0, 0, 0], 0.15) - - compas_rhino.clear_layer("Test::SphereArtist") - - for point in pcl.points: - sphere = tpl.transformed(Translation.from_vector(point)) - artist = SphereArtist(sphere, color=i_to_rgb(random.random()), layer="Test::SphereArtist") - artist.draw() - + layer : str, optional + The layer that should contain the drawing. """ - def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + def __init__(self, sphere, layer=None, **kwargs): + super(SphereArtist, self).__init__(shape=sphere, layer=layer, **kwargs) + + def draw(self, color=None, u=None, v=None): """Draw the sphere associated with the artist. Parameters ---------- + color : tuple of float, optional + The RGB color of the sphere. u : int, optional Number of faces in the "u" direction. - Default is ``10``. + Default is ``~SphereArtist.u``. v : int, optional Number of faces in the "v" direction. - Default is ``10``. - show_vertices : bool, optional - Default is ``False``. - show_edges : bool, optional - Default is ``False``. - show_faces : bool, optional - Default is ``True``. - join_faces : bool, optional - Default is ``True``. + Default is ``~SphereArtist.v``. Returns ------- list The GUIDs of the objects created in Rhino. """ + color = color or self.color + u = u or self.u + v = v or self.v vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) vertices = [list(vertex) for vertex in vertices] - guids = [] - if show_vertices: - points = [{'pos': point, 'color': self.color} for point in vertices] - guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - if show_edges: - lines = [] - seen = set() - for face in faces: - for u, v in pairwise(face + face[:1]): - if (u, v) not in seen: - seen.add((u, v)) - seen.add((v, u)) - lines.append({'start': vertices[u], 'end': vertices[v], 'color': self.color}) - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - if show_faces: - if join_faces: - guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.name, color=self.color, disjoint=True) - guids.append(guid) - else: - polygons = [{'points': [vertices[index] for index in face], 'color': self.color} for face in faces] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - self._guids = guids - return guids + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] diff --git a/src/compas_rhino/artists/torusartist.py b/src/compas_rhino/artists/torusartist.py index fe99c8364f2e..1db1da2b6847 100644 --- a/src/compas_rhino/artists/torusartist.py +++ b/src/compas_rhino/artists/torusartist.py @@ -2,95 +2,53 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from compas.artists import ShapeArtist +from .artist import RhinoArtist -class TorusArtist(ShapeArtist): +class TorusArtist(RhinoArtist, ShapeArtist): """Artist for drawing torus shapes. Parameters ---------- - shape : :class:`compas.geometry.Torus` + torus : :class:`compas.geometry.Torus` A COMPAS torus. - - Notes - ----- - See :class:`compas_rhino.artists.ShapeArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Torus - from compas.geometry import Translation - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import TorusArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - tpl = Torus([[0, 0, 0], [0, 0, 1]], 0.5, 0.2) - - compas_rhino.clear_layer("Test::TorusArtist") - - for point in pcl.points: - torus = tpl.transformed(Translation.from_vector(point)) - artist = TorusArtist(torus, color=i_to_rgb(random.random()), layer="Test::TorusArtist") - artist.draw() - + layer : str, optional + The layer that should contain the drawing. """ - def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + def __init__(self, torus, layer=None, **kwargs): + super(TorusArtist, self).__init__(shape=torus, layer=layer, **kwargs) + + def draw(self, color=None, u=None, v=None): """Draw the torus associated with the artist. Parameters ---------- + color : tuple of float, optional + The RGB color of the torus. u : int, optional Number of faces in the "u" direction. - Default is ``10``. + Default is ``~TorusArtist.u``. v : int, optional Number of faces in the "v" direction. - Default is ``10``. - show_vertices : bool, optional - Default is ``False``. - show_edges : bool, optional - Default is ``False``. - show_faces : bool, optional - Default is ``True``. - join_faces : bool, optional - Default is ``True``. + Default is ``~TorusArtist.v``. Returns ------- list The GUIDs of the objects created in Rhino. """ + color = color or self.color + u = u or self.u + v = v or self.v vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) vertices = [list(vertex) for vertex in vertices] - guids = [] - if show_vertices: - points = [{'pos': point, 'color': self.color} for point in vertices] - guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - if show_edges: - lines = [] - seen = set() - for face in faces: - for u, v in pairwise(face + face[:1]): - if (u, v) not in seen: - seen.add((u, v)) - seen.add((v, u)) - lines.append({'start': vertices[u], 'end': vertices[v], 'color': self.color}) - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - if show_faces: - if join_faces: - guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.name, color=self.color, disjoint=True) - guids.append(guid) - else: - polygons = [{'points': [vertices[index] for index in face], 'color': self.color} for face in faces] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - self._guids = guids - return guids + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] diff --git a/src/compas_rhino/artists/vectorartist.py b/src/compas_rhino/artists/vectorartist.py index b95a4a77a2cd..e85c9a33d121 100644 --- a/src/compas_rhino/artists/vectorartist.py +++ b/src/compas_rhino/artists/vectorartist.py @@ -4,47 +4,24 @@ from compas.geometry import Point import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from .artist import RhinoArtist -__all__ = ['VectorArtist'] - - -class VectorArtist(PrimitiveArtist): +class VectorArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing vectors. Parameters ---------- - primitive : :class:`compas.geometry.Vector` + vector : :class:`compas.geometry.Vector` A COMPAS vector. - - Notes - ----- - See :class:`compas_rhino.artists.PrimitiveArtist` for all other parameters. - - Examples - -------- - .. code-block:: python - - import random - from compas.geometry import Pointcloud - from compas.geometry import Vector - from compas.utilities import i_to_rgb - - import compas_rhino - from compas_rhino.artists import VectorArtist - - pcl = Pointcloud.from_bounds(10, 10, 10, 100) - - compas_rhino.clear_layer("Test::VectorArtist") - - for point in pcl.points: - vector = Vector(0, 0, 1) - artist = VectorArtist(vector, color=i_to_rgb(random.random()), layer="Test::VectorArtist") - artist.draw(point=point) - + layer : str, optional + The layer that should contain the drawing. """ + def __init__(self, vector, layer=None, **kwargs): + super(VectorArtist, self).__init__(primitive=vector, layer=layer, **kwargs) + def draw(self, point=None, show_point=False): """Draw the vector. @@ -71,9 +48,8 @@ def draw(self, point=None, show_point=False): end = list(end) guids = [] if show_point: - points = [{'pos': start, 'color': self.color, 'name': self.name}] + points = [{'pos': start, 'color': self.color, 'name': self.primitive.name}] guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - lines = [{'start': start, 'end': end, 'arrow': 'end', 'color': self.color, 'name': self.name}] + lines = [{'start': start, 'end': end, 'arrow': 'end', 'color': self.color, 'name': self.primitive.name}] guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - self._guids = guids return guids diff --git a/src/compas_rhino/artists/volmeshartist.py b/src/compas_rhino/artists/volmeshartist.py index 189ab200e957..13961409613b 100644 --- a/src/compas_rhino/artists/volmeshartist.py +++ b/src/compas_rhino/artists/volmeshartist.py @@ -5,19 +5,16 @@ from functools import partial import compas_rhino -from compas_rhino.artists._artist import BaseArtist - from compas.utilities import color_to_colordict from compas.geometry import centroid_points +from compas.artists import VolMeshArtist +from .artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['VolMeshArtist'] - - -class VolMeshArtist(BaseArtist): +class VolMeshArtist(RhinoArtist, VolMeshArtist): """Artist for drawing volmesh data structures. Parameters @@ -26,92 +23,73 @@ class VolMeshArtist(BaseArtist): A COMPAS volmesh. layer : str, optional The name of the layer that will contain the volmesh. - - Attributes - ---------- - volmesh : :class:`compas.datastructures.VolMesh` - The COMPAS volmesh associated with the artist. - layer : str - The layer in which the volmesh should be contained. - color_vertices : 3-tuple - Default color of the vertices. - color_edges : 3-tuple - Default color of the edges. - color_faces : 3-tuple - Default color of the faces. - """ - def __init__(self, volmesh, layer=None): - super(VolMeshArtist, self).__init__() - self._volmesh = None - self._vertex_xyz = None - self.volmesh = volmesh - self.layer = layer - self.color_vertices = (255, 255, 255) - self.color_edges = (0, 0, 0) - self.color_faces = (210, 210, 210) - self.color_cells = (255, 0, 0) - - @property - def volmesh(self): - return self._volmesh - - @volmesh.setter - def volmesh(self, volmesh): - self._volmesh = volmesh - self._vertex_xyz = None - - @property - def vertex_xyz(self): - if not self._vertex_xyz: - self._vertex_xyz = {vertex: self.volmesh.vertex_attributes(vertex, 'xyz') for vertex in self.volmesh.vertices()} - return self._vertex_xyz - - @vertex_xyz.setter - def vertex_xyz(self, vertex_xyz): - self._vertex_xyz = vertex_xyz - - # ========================================================================== - # clear - # ========================================================================== + def __init__(self, volmesh, layer=None, **kwargs): + super(VolMeshArtist, self).__init__(volmesh=volmesh, layer=layer, **kwargs) def clear_by_name(self): """Clear all objects in the "namespace" of the associated volmesh.""" guids = compas_rhino.get_objects(name="{}.*".format(self.volmesh.name)) compas_rhino.delete_objects(guids, purge=True) - def clear_layer(self): - """Clear the main layer of the artist.""" - if self.layer: - compas_rhino.clear_layer(self.layer) - # ========================================================================== # draw # ========================================================================== - def draw(self, settings=None): - """Draw the volmesh using the chosen visualisation settings. + def draw(self, vertices=None, edges=None, faces=None, cells=None, vertexcolor=None, edgecolor=None, facecolor=None, cellcolor=None): + """Draw the network using the chosen visualisation settings. Parameters ---------- - settings : dict, optional - Dictionary of visualisation settings that will be merged with the settings of the artist. + vertices : list, optional + A list of vertices to draw. + Default is ``None``, in which case all vertices are drawn. + edges : list, optional + A list of edges to draw. + The default is ``None``, in which case all edges are drawn. + faces : list, optional + A selection of faces to draw. + The default is ``None``, in which case all faces are drawn. + cells : list, optional + A selection of cells to draw. + The default is ``None``, in which case all cells are drawn. + vertexcolor : tuple or dict of tuple, optional + The color specififcation for the vertices. + The default color is the value of ``~VolMeshArtist.default_vertexcolor``. + edgecolor : tuple or dict of tuple, optional + The color specififcation for the edges. + The default color is the value of ``~VolMeshArtist.default_edgecolor``. + facecolor : tuple or dict of tuple, optional + The color specififcation for the faces. + The default color is the value of ``~VolMeshArtist.default_facecolor``. + cellcolor : tuple or dict of tuple, optional + The color specififcation for the cells. + The default color is the value of ``~VolMeshArtist.default_cellcolor``. + + Returns + ------- + list + The GUIDs of the created Rhino objects. """ - raise NotImplementedError + guids = self.draw_vertices(vertices=vertices, color=vertexcolor) + guids += self.draw_edges(edges=edges, color=edgecolor) + guids += self.draw_faces(faces=faces, color=facecolor) + guids += self.draw_cells(cells=cells, color=cellcolor) + return guids def draw_vertices(self, vertices=None, color=None): """Draw a selection of vertices. Parameters ---------- - vertices : list + vertices : list, optional A list of vertices to draw. Default is ``None``, in which case all vertices are drawn. color : str, tuple, dict The color specififcation for the vertices. - The default color of the vertices is ``(255, 255, 255)``. + The default color of the vertices is ``~VolMeshArtist.default_vertexcolor``. Returns ------- @@ -119,15 +97,16 @@ def draw_vertices(self, vertices=None, color=None): The GUIDs of the created Rhino objects. """ + self.vertex_color = color vertices = vertices or list(self.volmesh.vertices()) vertex_xyz = self.vertex_xyz - vertex_color = colordict(color, vertices, default=self.color_vertices) points = [] for vertex in vertices: points.append({ 'pos': vertex_xyz[vertex], 'name': "{}.vertex.{}".format(self.volmesh.name, vertex), - 'color': vertex_color[vertex]}) + 'color': self.vertex_color.get(vertex, self.default_vertexcolor) + }) return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) def draw_edges(self, edges=None, color=None): @@ -135,12 +114,12 @@ def draw_edges(self, edges=None, color=None): Parameters ---------- - edges : list + edges : list, optional A list of edges to draw. The default is ``None``, in which case all edges are drawn. color : str, tuple, dict The color specififcation for the edges. - The default color is ``(0, 0, 0)``. + The default color is ``~VolMeshArtist.default_edgecolor``. Returns ------- @@ -148,16 +127,17 @@ def draw_edges(self, edges=None, color=None): The GUIDs of the created Rhino objects. """ + self.edge_color = color edges = edges or list(self.volmesh.edges()) vertex_xyz = self.vertex_xyz - edge_color = colordict(color, edges, default=self.color_edges) lines = [] for edge in edges: lines.append({ 'start': vertex_xyz[edge[0]], 'end': vertex_xyz[edge[1]], - 'color': edge_color[edge], - 'name': "{}.edge.{}-{}".format(self.volmesh.name, *edge)}) + 'color': self.edge_color.get(edge, self.default_edgecolor), + 'name': "{}.edge.{}-{}".format(self.volmesh.name, *edge) + }) return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) def draw_faces(self, faces=None, color=None): @@ -165,12 +145,12 @@ def draw_faces(self, faces=None, color=None): Parameters ---------- - faces : list + faces : list, optional A list of faces to draw. The default is ``None``, in which case all faces are drawn. color : str, tuple, dict The color specififcation for the faces. - The default color is ``(210, 210, 210)``. + The default color is ``~VolMeshArtist.default_facecolor``. Returns ------- @@ -178,15 +158,16 @@ def draw_faces(self, faces=None, color=None): The GUIDs of the created Rhino objects. """ + self.face_color = color faces = faces or list(self.volmesh.faces()) vertex_xyz = self.vertex_xyz - face_color = colordict(color, faces, default=self.color_faces) facets = [] for face in faces: facets.append({ 'points': [vertex_xyz[vertex] for vertex in self.volmesh.halfface_vertices(face)], 'name': "{}.face.{}".format(self.volmesh.name, face), - 'color': face_color[face]}) + 'color': self.face_color.get(face, self.default_facecolor) + }) return compas_rhino.draw_faces(facets, layer=self.layer, clear=False, redraw=False) def draw_cells(self, cells=None, color=None): @@ -199,7 +180,7 @@ def draw_cells(self, cells=None, color=None): The default is ``None``, in which case all cells are drawn. color : str, tuple, dict The color specififcation for the cells. - The default color is ``(255, 0, 0)``. + The default color is ``~VolMeshArtist.default_cellcolor``. Returns ------- @@ -208,9 +189,9 @@ def draw_cells(self, cells=None, color=None): Every cell is drawn as an individual mesh. """ + self.cell_color = color cells = cells or list(self.volmesh.cells()) vertex_xyz = self.vertex_xyz - cell_color = colordict(color, cells, default=self.color_cells) meshes = [] for cell in cells: cell_faces = [] @@ -218,12 +199,13 @@ def draw_cells(self, cells=None, color=None): cell_faces.append({ 'points': [vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(fkey)], 'name': "{}.cell.{}.face.{}".format(self.volmesh.name, cell, fkey), - 'color': cell_color[cell]}) + 'color': self.cell_color.get(cell, self.default_cellcolor) + }) guids = compas_rhino.draw_faces(cell_faces, layer=self.layer, clear=False, redraw=False) guid = compas_rhino.rs.JoinMeshes(guids, delete_input=True) compas_rhino.rs.ObjectLayer(guid, self.layer) compas_rhino.rs.ObjectName(guid, '{}.cell.{}'.format(self.volmesh.name, cell)) - compas_rhino.rs.ObjectColor(guid, cell_color[cell]) + compas_rhino.rs.ObjectColor(guid, self.cell_color.get(cell, self.default_cellcolor)) meshes.append(guid) return meshes @@ -265,7 +247,8 @@ def draw_vertexlabels(self, text=None, color=None): 'pos': vertex_xyz[vertex], 'name': "{}.vertexlabel.{}".format(self.volmesh.name, vertex), 'color': vertex_color[vertex], - 'text': vertex_text[vertex]}) + 'text': vertex_text[vertex] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) def draw_edgelabels(self, text=None, color=None): @@ -300,7 +283,8 @@ def draw_edgelabels(self, text=None, color=None): 'pos': centroid_points([vertex_xyz[edge[0]], vertex_xyz[edge[1]]]), 'name': "{}.edgelabel.{}-{}".format(self.volmesh.name, *edge), 'color': edge_color[edge], - 'text': edge_text[edge]}) + 'text': edge_text[edge] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) def draw_facelabels(self, text=None, color=None): @@ -337,7 +321,8 @@ def draw_facelabels(self, text=None, color=None): 'pos': centroid_points([vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)]), 'name': "{}.facelabel.{}".format(self.volmesh.name, face), 'color': face_color[face], - 'text': face_text[face]}) + 'text': face_text[face] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) def draw_celllabels(self, text=None, color=None): @@ -374,5 +359,6 @@ def draw_celllabels(self, text=None, color=None): 'pos': centroid_points([vertex_xyz[vertex] for vertex in self.volmesh.cell_vertices(cell)]), 'name': "{}.facelabel.{}".format(self.volmesh.name, cell), 'color': cell_color[cell], - 'text': cell_text[cell]}) + 'text': cell_text[cell] + }) return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) diff --git a/src/compas_rhino/objects/__init__.py b/src/compas_rhino/objects/__init__.py index f4beb33ccd46..e0c2e3e782c4 100644 --- a/src/compas_rhino/objects/__init__.py +++ b/src/compas_rhino/objects/__init__.py @@ -5,24 +5,6 @@ .. currentmodule:: compas_rhino.objects -.. .. rst-class:: lead - -.. code-block:: python - - import compas - from compas.datastructures import Mesh - from compas_rhino.objects import MeshObject - - mesh = Mesh.from_off(compas.get('tubemesh.off')) - - meshobject = MeshObject(mesh, name='MeshObject', layer='COMPAS::MeshObject') - meshobject.draw() - - vertices = meshobject.select_vertices() - - if vertices and meshobject.modify_vertices(vertices): - meshobject.draw() - NetworkObject ============= diff --git a/src/compas_rhino/objects/_object.py b/src/compas_rhino/objects/_object.py index a98b5b2d139f..c3916cbac2cb 100644 --- a/src/compas_rhino/objects/_object.py +++ b/src/compas_rhino/objects/_object.py @@ -3,7 +3,7 @@ from __future__ import print_function from uuid import uuid4 -from compas_rhino.artists import BaseArtist +from compas_rhino.artists import RhinoArtist __all__ = ['BaseObject'] @@ -84,7 +84,7 @@ def item(self): @item.setter def item(self, item): self._item = item - self._artist = BaseArtist.build(item) + self._artist = RhinoArtist.build(item) @property def artist(self): diff --git a/src/compas_rhino/utilities/drawing.py b/src/compas_rhino/utilities/drawing.py index cd0e981786c9..2d88a04f935a 100644 --- a/src/compas_rhino/utilities/drawing.py +++ b/src/compas_rhino/utilities/drawing.py @@ -88,7 +88,7 @@ def wrap_drawfunc(f): def wrapper(*args, **kwargs): layer = kwargs.get('layer', None) clear = kwargs.get('clear', False) - redraw = kwargs.get('redraw', True) + redraw = kwargs.get('redraw', False) if layer: if not rs.IsLayer(layer): create_layers_from_path(layer)