From df11766c2b484908d7c6291c6378715e2678c612 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 10:46:06 +0200 Subject: [PATCH 01/71] ignore dev folder --- .gitignore | 2 + src/compas/artists/__init__.py | 0 src/compas/artists/artist.py | 77 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 src/compas/artists/__init__.py create mode 100644 src/compas/artists/artist.py diff --git a/.gitignore b/.gitignore index 5d36d6beb8df..de3e2cbfef66 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,5 @@ data/ctralie # Grasshopper generated objects src/compas_ghpython/components/**/*.ghuser + +dev diff --git a/src/compas/artists/__init__.py b/src/compas/artists/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/compas/artists/artist.py b/src/compas/artists/artist.py new file mode 100644 index 000000000000..79e35a2b5c8d --- /dev/null +++ b/src/compas/artists/artist.py @@ -0,0 +1,77 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from compas.plugins import pluggable + +_ITEM_ARTIST = {} + + +@pluggable(category='factories') +def new_artist(cls, *args, **kwargs): + pass + + +# @plugin(category='factories', pluggable_name='new_artist') +# def new_artist_rhino(cls, *args, **kwargs): +# data = args[0] +# cls = Artist.data_artist[type(data)] +# return super(Artist, cls).__new__(cls) + + +class Artist(object): + """Base class for all artists. + + Parameters + ---------- + item: :class:`compas.data.Data` + The data item. + + Attributes + ---------- + item: :class:`compas.data.Data` + The data item. + """ + + def __new__(cls, *args, **kwargs): + return new_artist(cls, *args, **kwargs) + + def __init__(self, item, *args, **kwargs): + super(Artist, self).__init__() + self.item = item + self.args = args + self.kwargs = kwargs + + @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.scene.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): + raise NotImplementedError + + @staticmethod + def draw_collection(collection): + raise NotImplementedError From aeeac4109c1870f656d72e2303789880cad2ec81 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 10:49:08 +0200 Subject: [PATCH 02/71] base artist --- docs/api/compas.artists.rst | 2 ++ src/compas/__init__.py | 1 + src/compas/artists/__init__.py | 26 ++++++++++++++++++++++++++ src/compas/artists/artist.py | 33 +++++---------------------------- 4 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 docs/api/compas.artists.rst 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/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 index e69de29bb2d1..6b291570349c 100644 --- a/src/compas/artists/__init__.py +++ b/src/compas/artists/__init__.py @@ -0,0 +1,26 @@ +""" +******************************************************************************** +artists +******************************************************************************** + +.. currentmodule:: compas.artists + +Classes +======= + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + Artist + +""" +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from .artist import Artist # noqa: F401 + +__all__ = [ + 'Artist', +] diff --git a/src/compas/artists/artist.py b/src/compas/artists/artist.py index 79e35a2b5c8d..f61a19755e41 100644 --- a/src/compas/artists/artist.py +++ b/src/compas/artists/artist.py @@ -4,47 +4,24 @@ from compas.plugins import pluggable -_ITEM_ARTIST = {} - @pluggable(category='factories') def new_artist(cls, *args, **kwargs): - pass - - -# @plugin(category='factories', pluggable_name='new_artist') -# def new_artist_rhino(cls, *args, **kwargs): -# data = args[0] -# cls = Artist.data_artist[type(data)] -# return super(Artist, cls).__new__(cls) + raise NotImplementedError class Artist(object): """Base class for all artists. - - Parameters - ---------- - item: :class:`compas.data.Data` - The data item. - - Attributes - ---------- - item: :class:`compas.data.Data` - The data item. """ + ITEM_ARTIST = {} + def __new__(cls, *args, **kwargs): return new_artist(cls, *args, **kwargs) - def __init__(self, item, *args, **kwargs): - super(Artist, self).__init__() - self.item = item - self.args = args - self.kwargs = kwargs - @staticmethod def register(item_type, artist_type): - _ITEM_ARTIST[item_type] = artist_type + Artist.ITEM_ARTIST[item_type] = artist_type @staticmethod def build(item, **kwargs): @@ -62,7 +39,7 @@ def build(item, **kwargs): 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_type = Artist.ITEM_ARTIST[type(item)] artist = artist_type(item, **kwargs) return artist From e8e45159a2f9c0fc63d3363872717dc823ba70fb Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 10:50:18 +0200 Subject: [PATCH 03/71] rebase rhino artists --- src/compas_rhino/__init__.py | 1 + src/compas_rhino/artists/__init__.py | 78 +++++++++----------- src/compas_rhino/artists/_artist.py | 33 +-------- src/compas_rhino/artists/_primitiveartist.py | 4 +- src/compas_rhino/artists/_shapeartist.py | 4 +- src/compas_rhino/artists/meshartist.py | 4 +- src/compas_rhino/artists/networkartist.py | 4 +- src/compas_rhino/artists/robotmodelartist.py | 4 +- src/compas_rhino/artists/volmeshartist.py | 4 +- 9 files changed, 51 insertions(+), 85 deletions(-) diff --git a/src/compas_rhino/__init__.py b/src/compas_rhino/__init__.py index 443dd01a5020..80424be8e041 100644 --- a/src/compas_rhino/__init__.py +++ b/src/compas_rhino/__init__.py @@ -213,4 +213,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..61cc0e692de3 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,16 @@ :toctree: generated/ :nosignatures: - BaseArtist + RhinoArtist PrimitiveArtist ShapeArtist """ from __future__ import absolute_import +from compas.plugins import plugin +from compas.artists import Artist + from compas.geometry import Circle from compas.geometry import Frame from compas.geometry import Line @@ -116,9 +101,9 @@ from compas.robots import RobotModel -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 ._primitiveartist import PrimitiveArtist +from ._shapeartist import ShapeArtist from .circleartist import CircleArtist from .frameartist import FrameArtist @@ -143,32 +128,41 @@ 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) +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) -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) +RhinoArtist.register(Mesh, MeshArtist) +RhinoArtist.register(Network, NetworkArtist) +RhinoArtist.register(VolMesh, VolMeshArtist) -BaseArtist.register(Mesh, MeshArtist) -BaseArtist.register(Network, NetworkArtist) -BaseArtist.register(VolMesh, VolMeshArtist) +RhinoArtist.register(RobotModel, RobotModelArtist) -BaseArtist.register(RobotModel, RobotModelArtist) + +@plugin(category='factories', pluggable_name='new_artist', requires=['Rhino']) +def new_artist_rhino(cls, *args, **kwargs): + data = args[0] + cls = Artist.ITEM_ARTIST[type(data)] + return super(Artist, cls).__new__(cls) __all__ = [ - 'BaseArtist', + 'new_artist_rhino', + + 'RhinoArtist', 'PrimitiveArtist', 'ShapeArtist', 'CircleArtist', diff --git a/src/compas_rhino/artists/_artist.py b/src/compas_rhino/artists/_artist.py index 1f445292b07a..521ac28c8072 100644 --- a/src/compas_rhino/artists/_artist.py +++ b/src/compas_rhino/artists/_artist.py @@ -3,15 +3,10 @@ from __future__ import division import compas_rhino +from compas.artists import Artist -__all__ = ["BaseArtist"] - - -_ITEM_ARTIST = {} - - -class BaseArtist(object): +class RhinoArtist(Artist): """Base class for all Rhino artists. Attributes @@ -24,30 +19,6 @@ class BaseArtist(object): 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 diff --git a/src/compas_rhino/artists/_primitiveartist.py b/src/compas_rhino/artists/_primitiveartist.py index 7f301a61193f..590b2f25959b 100644 --- a/src/compas_rhino/artists/_primitiveartist.py +++ b/src/compas_rhino/artists/_primitiveartist.py @@ -3,13 +3,13 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._artist import BaseArtist +from compas_rhino.artists._artist import RhinoArtist __all__ = ["PrimitiveArtist"] -class PrimitiveArtist(BaseArtist): +class PrimitiveArtist(RhinoArtist): """Base class for artists for geometry primitives. Parameters diff --git a/src/compas_rhino/artists/_shapeartist.py b/src/compas_rhino/artists/_shapeartist.py index d8dc19f74e15..cb6494fdd17b 100644 --- a/src/compas_rhino/artists/_shapeartist.py +++ b/src/compas_rhino/artists/_shapeartist.py @@ -4,12 +4,12 @@ import compas_rhino from compas.datastructures import Mesh -from compas_rhino.artists._artist import BaseArtist +from compas_rhino.artists._artist import RhinoArtist __all__ = ['ShapeArtist'] -class ShapeArtist(BaseArtist): +class ShapeArtist(RhinoArtist): """Base class for artists for geometric shapes. Parameters diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index e309efd4b311..a61ce8ae2087 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -5,7 +5,7 @@ from functools import partial import compas_rhino -from compas_rhino.artists._artist import BaseArtist +from compas_rhino.artists._artist import RhinoArtist from compas.utilities import color_to_colordict from compas.utilities import pairwise @@ -21,7 +21,7 @@ __all__ = ['MeshArtist'] -class MeshArtist(BaseArtist): +class MeshArtist(RhinoArtist): """Artists for drawing mesh data structures. Parameters diff --git a/src/compas_rhino/artists/networkartist.py b/src/compas_rhino/artists/networkartist.py index e5ada0b82b35..441e19e85ca8 100644 --- a/src/compas_rhino/artists/networkartist.py +++ b/src/compas_rhino/artists/networkartist.py @@ -4,7 +4,7 @@ from functools import partial import compas_rhino -from compas_rhino.artists._artist import BaseArtist +from compas_rhino.artists._artist import RhinoArtist from compas.geometry import centroid_points from compas.utilities import color_to_colordict @@ -15,7 +15,7 @@ __all__ = ['NetworkArtist'] -class NetworkArtist(BaseArtist): +class NetworkArtist(RhinoArtist): """Artist for drawing network data structures. Parameters diff --git a/src/compas_rhino/artists/robotmodelartist.py b/src/compas_rhino/artists/robotmodelartist.py index b7d50024a2e7..5473e93f99f5 100644 --- a/src/compas_rhino/artists/robotmodelartist.py +++ b/src/compas_rhino/artists/robotmodelartist.py @@ -16,7 +16,7 @@ from compas.robots.base_artist import BaseRobotModelArtist 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__ = [ @@ -24,7 +24,7 @@ ] -class RobotModelArtist(BaseRobotModelArtist, BaseArtist): +class RobotModelArtist(BaseRobotModelArtist, RhinoArtist): """Visualizer for robots inside a Rhino environment. Parameters diff --git a/src/compas_rhino/artists/volmeshartist.py b/src/compas_rhino/artists/volmeshartist.py index 189ab200e957..74468193e128 100644 --- a/src/compas_rhino/artists/volmeshartist.py +++ b/src/compas_rhino/artists/volmeshartist.py @@ -5,7 +5,7 @@ from functools import partial import compas_rhino -from compas_rhino.artists._artist import BaseArtist +from compas_rhino.artists._artist import RhinoArtist from compas.utilities import color_to_colordict from compas.geometry import centroid_points @@ -17,7 +17,7 @@ __all__ = ['VolMeshArtist'] -class VolMeshArtist(BaseArtist): +class VolMeshArtist(RhinoArtist): """Artist for drawing volmesh data structures. Parameters From 2ac21c245d401a80455a62a7591ffb488382ea81 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 10:50:27 +0200 Subject: [PATCH 04/71] rebase blender artists --- src/compas_blender/__init__.py | 1 + src/compas_blender/artists/__init__.py | 27 ++++++- src/compas_blender/artists/_artist.py | 76 ------------------- src/compas_blender/artists/artist.py | 34 +++++++++ src/compas_blender/artists/frameartist.py | 9 +-- src/compas_blender/artists/meshartist.py | 9 +-- src/compas_blender/artists/networkartist.py | 11 +-- .../artists/robotmodelartist.py | 6 +- src/compas_blender/artists/volmeshartist.py | 7 +- 9 files changed, 69 insertions(+), 111 deletions(-) delete mode 100644 src/compas_blender/artists/_artist.py create mode 100644 src/compas_blender/artists/artist.py diff --git a/src/compas_blender/__init__.py b/src/compas_blender/__init__.py index ded5a5c28627..63c6aed80408 100644 --- a/src/compas_blender/__init__.py +++ b/src/compas_blender/__init__.py @@ -47,4 +47,5 @@ def clear(): __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..4d31ee44e72c 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -14,7 +14,7 @@ .. autosummary:: :toctree: generated/ - BaseArtist + BlenderArtist Classes @@ -29,8 +29,15 @@ RobotModelArtist """ +from compas.plugins import plugin +from compas.artists import Artist -from ._artist import BaseArtist # noqa: F401 +from compas.geometry import Frame +from compas.datastructures import Mesh +from compas.datastructures import Network +from compas.robots import RobotModel + +from .artist import BlenderArtist # noqa: F401 from .frameartist import FrameArtist from .meshartist import MeshArtist from .networkartist import NetworkArtist @@ -39,7 +46,23 @@ RobotModelArtist ) + +Artist.register(Frame, FrameArtist) +Artist.register(Mesh, MeshArtist) +Artist.register(Network, NetworkArtist) +Artist.register(RobotModel, RobotModelArtist) + + +@plugin(category='factories', pluggable_name='new_artist', requires=['bpy']) +def new_artist_blender(cls, *args, **kwargs): + data = args[0] + cls = Artist.ITEM_ARTIST[type(data)] + return super(Artist, cls).__new__(cls) + + __all__ = [ + 'new_artist_blender', + 'FrameArtist', 'NetworkArtist', 'MeshArtist', 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..9954f0be4fad --- /dev/null +++ b/src/compas_blender/artists/artist.py @@ -0,0 +1,34 @@ +import bpy +import abc +import compas_blender + +from compas.artists import Artist + + +class BlenderArtist(Artist, 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 = [] + + def draw(self): + """Draw the item.""" + raise NotImplementedError + + 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/frameartist.py b/src/compas_blender/artists/frameartist.py index d31a8bb297a8..2d7f01d98e86 100644 --- a/src/compas_blender/artists/frameartist.py +++ b/src/compas_blender/artists/frameartist.py @@ -1,11 +1,8 @@ import compas_blender -from compas_blender.artists import BaseArtist +from .artist import BlenderArtist -__all__ = ['FrameArtist'] - - -class FrameArtist(BaseArtist): +class FrameArtist(BlenderArtist): """Artist for drawing frames. Parameters @@ -56,7 +53,7 @@ class FrameArtist(BaseArtist): """ def __init__(self, frame, collection=None, scale=1.0): - super(FrameArtist, self).__init__() + super().__init__() self.collection = collection self.frame = frame self.scale = scale or 1.0 diff --git a/src/compas_blender/artists/meshartist.py b/src/compas_blender/artists/meshartist.py index 299a7d40bec3..d691bb5fd4bd 100644 --- a/src/compas_blender/artists/meshartist.py +++ b/src/compas_blender/artists/meshartist.py @@ -1,5 +1,3 @@ -# from __future__ import annotations - from functools import partial import compas_blender @@ -7,16 +5,13 @@ 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 .artist import BlenderArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=True) -__all__ = ['MeshArtist'] - - -class MeshArtist(BaseArtist): +class MeshArtist(BlenderArtist): """A mesh artist defines functionality for visualising COMPAS meshes in Blender. Parameters diff --git a/src/compas_blender/artists/networkartist.py b/src/compas_blender/artists/networkartist.py index 1e721cecc318..52f347137c36 100644 --- a/src/compas_blender/artists/networkartist.py +++ b/src/compas_blender/artists/networkartist.py @@ -1,21 +1,14 @@ -# from __future__ import annotations - from functools import partial import compas_blender -from compas_blender.artists._artist import BaseArtist from compas.utilities import color_to_colordict +from .artist import BlenderArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=True) -__all__ = [ - 'NetworkArtist', -] - - -class NetworkArtist(BaseArtist): +class NetworkArtist(BlenderArtist): """Artist for COMPAS network objects. Parameters diff --git a/src/compas_blender/artists/robotmodelartist.py b/src/compas_blender/artists/robotmodelartist.py index d357479f74aa..17c7e53d0115 100644 --- a/src/compas_blender/artists/robotmodelartist.py +++ b/src/compas_blender/artists/robotmodelartist.py @@ -4,10 +4,6 @@ import compas_blender from compas.robots.base_artist import BaseRobotModelArtist -__all__ = [ - 'RobotModelArtist', -] - class RobotModelArtist(BaseRobotModelArtist): """Visualizer for robot models inside a Blender environment. @@ -20,7 +16,7 @@ class RobotModelArtist(BaseRobotModelArtist): def __init__(self, model, collection=None): self.collection = collection - super(RobotModelArtist, self).__init__(model) + super().__init__(model) def transform(self, native_mesh, transformation): native_mesh.matrix_world = mathutils.Matrix(transformation.matrix) @ native_mesh.matrix_world diff --git a/src/compas_blender/artists/volmeshartist.py b/src/compas_blender/artists/volmeshartist.py index c1df1da783bb..5b025c76a236 100644 --- a/src/compas_blender/artists/volmeshartist.py +++ b/src/compas_blender/artists/volmeshartist.py @@ -1,9 +1,4 @@ -from compas_blender.artists.meshartist import MeshArtist - - -__all__ = [ - 'VolMeshArtist', -] +from .meshartist import MeshArtist class VolMeshArtist(MeshArtist): From e4dc09927e8b2cdd7ec8d363e42c4653684fd103 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 10:59:11 +0200 Subject: [PATCH 05/71] rhinoartist instead of baseartist --- src/compas_rhino/objects/__init__.py | 18 ------------------ src/compas_rhino/objects/_object.py | 4 ++-- 2 files changed, 2 insertions(+), 20 deletions(-) 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): From 97be6e79a3c5cc92fbe4564d691646dc5bf3b3bf Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 10:59:18 +0200 Subject: [PATCH 06/71] clean up --- docs/_templates/autosummary/base.rst | 5 ----- docs/_templates/autosummary/class.rst | 28 -------------------------- docs/_templates/autosummary/module.rst | 3 --- docs/conf.py | 2 +- 4 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 docs/_templates/autosummary/base.rst delete mode 100644 docs/_templates/autosummary/class.rst delete mode 100644 docs/_templates/autosummary/module.rst 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/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 From 59c8cc3f4cf56f2adb51deb7df26c5ac01cde2c1 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 11:01:51 +0200 Subject: [PATCH 07/71] log --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d471509055e..e417b4a36222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased -* Added `Plane.offset` + ### Added * Added `draw_vertexlabels`, `draw_edgelabels`, `draw_facelabels`, `draw_vertexnormals`, and `draw_facenormals` to `compas_blender.artists.MeshArtist`. +* 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`. ### 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`. ### Removed From 042ebb9d3fb1e0414372b48651ff08474a8d2e35 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 11:49:00 +0200 Subject: [PATCH 08/71] add exception for missing artist --- src/compas/artists/__init__.py | 14 +++++++++++++- src/compas/artists/exceptions.py | 7 +++++++ src/compas_blender/artists/__init__.py | 8 +++++--- src/compas_rhino/artists/__init__.py | 8 +++++--- 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/compas/artists/exceptions.py diff --git a/src/compas/artists/__init__.py b/src/compas/artists/__init__.py index 6b291570349c..81de4740bb1d 100644 --- a/src/compas/artists/__init__.py +++ b/src/compas/artists/__init__.py @@ -14,13 +14,25 @@ Artist + +Exceptions +========== + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + DataArtistNotRegistered + """ from __future__ import print_function from __future__ import absolute_import from __future__ import division -from .artist import Artist # noqa: F401 +from .exceptions import DataArtistNotRegistered +from .artist import Artist __all__ = [ + 'DataArtistNotRegistered', 'Artist', ] diff --git a/src/compas/artists/exceptions.py b/src/compas/artists/exceptions.py new file mode 100644 index 000000000000..7c9a92a8d5ff --- /dev/null +++ b/src/compas/artists/exceptions.py @@ -0,0 +1,7 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + + +class DataArtistNotRegistered(Exception): + pass diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 4d31ee44e72c..647dbc13dad8 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -31,6 +31,7 @@ """ from compas.plugins import plugin from compas.artists import Artist +from compas.artists import DataArtistNotRegistered from compas.geometry import Frame from compas.datastructures import Mesh @@ -56,13 +57,14 @@ @plugin(category='factories', pluggable_name='new_artist', requires=['bpy']) def new_artist_blender(cls, *args, **kwargs): data = args[0] - cls = Artist.ITEM_ARTIST[type(data)] + dtype = type(data) + if dtype not in Artist.ITEM_ARTIST: + raise DataArtistNotRegistered('No Blender artist is registered for this data type: {}'.format(dtype)) + cls = Artist.ITEM_ARTIST[dtype] return super(Artist, cls).__new__(cls) __all__ = [ - 'new_artist_blender', - 'FrameArtist', 'NetworkArtist', 'MeshArtist', diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 61cc0e692de3..3c09d3b22631 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -77,6 +77,7 @@ from compas.plugins import plugin from compas.artists import Artist +from compas.artists import DataArtistNotRegistered from compas.geometry import Circle from compas.geometry import Frame @@ -155,13 +156,14 @@ @plugin(category='factories', pluggable_name='new_artist', requires=['Rhino']) def new_artist_rhino(cls, *args, **kwargs): data = args[0] - cls = Artist.ITEM_ARTIST[type(data)] + dtype = type(data) + if dtype not in Artist.ITEM_ARTIST: + raise DataArtistNotRegistered('No Rhino artist is registered for this data type: {}'.format(dtype)) + cls = Artist.ITEM_ARTIST[dtype] return super(Artist, cls).__new__(cls) __all__ = [ - 'new_artist_rhino', - 'RhinoArtist', 'PrimitiveArtist', 'ShapeArtist', From d1f8e3675a8bcb15e6abb5dfa6584eec81dc06d8 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 11:54:42 +0200 Subject: [PATCH 09/71] log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e417b4a36222..de02dc8e8ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * 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'. ### Changed From 1143b683ad51d19fdd256a57f7a15cf6a97b3612 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 19:03:54 +0200 Subject: [PATCH 10/71] home brewed version of abstractmethod mechanism --- src/compas/artists/artist.py | 9 ++++++--- src/compas_blender/artists/__init__.py | 6 ++++++ src/compas_blender/artists/artist.py | 7 +------ src/compas_rhino/artists/__init__.py | 6 ++++++ src/compas_rhino/artists/_artist.py | 3 --- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/compas/artists/artist.py b/src/compas/artists/artist.py index f61a19755e41..eb74b86b9829 100644 --- a/src/compas/artists/artist.py +++ b/src/compas/artists/artist.py @@ -2,6 +2,7 @@ from __future__ import division from __future__ import print_function +from abc import abstractmethod from compas.plugins import pluggable @@ -35,17 +36,19 @@ def build(item, **kwargs): Returns ------- - :class:`compas.scene.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``. + :class:`compas.artists.Artist` + An artist of the type matching the provided item according to the item-artist map ``~Artist.ITEM_ARTIST``. + The map is created by registering item-artist type pairs using ``~Artist.register``. """ artist_type = Artist.ITEM_ARTIST[type(item)] artist = artist_type(item, **kwargs) return artist + @abstractmethod def draw(self): raise NotImplementedError + @abstractmethod def redraw(self): raise NotImplementedError diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 647dbc13dad8..78494dbdc441 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -29,6 +29,8 @@ RobotModelArtist """ +import inspect + from compas.plugins import plugin from compas.artists import Artist from compas.artists import DataArtistNotRegistered @@ -61,6 +63,10 @@ def new_artist_blender(cls, *args, **kwargs): if dtype not in Artist.ITEM_ARTIST: raise DataArtistNotRegistered('No Blender artist is registered for this data type: {}'.format(dtype)) cls = Artist.ITEM_ARTIST[dtype] + 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) diff --git a/src/compas_blender/artists/artist.py b/src/compas_blender/artists/artist.py index 9954f0be4fad..fddde69ded7d 100644 --- a/src/compas_blender/artists/artist.py +++ b/src/compas_blender/artists/artist.py @@ -1,11 +1,10 @@ import bpy -import abc import compas_blender from compas.artists import Artist -class BlenderArtist(Artist, abc.ABC): +class BlenderArtist(Artist): """Base class for all Blender artists. Attributes @@ -18,10 +17,6 @@ class BlenderArtist(Artist, abc.ABC): def __init__(self): self.objects = [] - def draw(self): - """Draw the item.""" - raise NotImplementedError - def redraw(self): """Trigger a redraw.""" bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 3c09d3b22631..8e826a6080a9 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -75,6 +75,8 @@ """ from __future__ import absolute_import +import inspect + from compas.plugins import plugin from compas.artists import Artist from compas.artists import DataArtistNotRegistered @@ -160,6 +162,10 @@ def new_artist_rhino(cls, *args, **kwargs): if dtype not in Artist.ITEM_ARTIST: raise DataArtistNotRegistered('No Rhino artist is registered for this data type: {}'.format(dtype)) cls = Artist.ITEM_ARTIST[dtype] + for name, value in inspect.getmembers(cls): + if inspect.ismethod(value): + if hasattr(value, '__isabstractmethod__'): + raise Exception('Abstract method not implemented') return super(Artist, cls).__new__(cls) diff --git a/src/compas_rhino/artists/_artist.py b/src/compas_rhino/artists/_artist.py index 521ac28c8072..806553f25b2a 100644 --- a/src/compas_rhino/artists/_artist.py +++ b/src/compas_rhino/artists/_artist.py @@ -19,9 +19,6 @@ class RhinoArtist(Artist): def __init__(self): self._guids = [] - def draw(self): - raise NotImplementedError - def redraw(self): compas_rhino.rs.EnableRedraw(True) compas_rhino.rs.Redraw() From 1b04504fe221a1af5867f842691c785d899c77f5 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 20:22:22 +0200 Subject: [PATCH 11/71] stubs for specialised base artists --- src/compas/artists/assemblyartist.py | 0 src/compas/artists/meshartist.py | 0 src/compas/artists/networkartist.py | 0 src/compas/artists/primitiveartist.py | 0 src/compas/artists/robotmodelartist.py | 0 src/compas/artists/shapeartist.py | 0 src/compas/artists/volmeshartist.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/compas/artists/assemblyartist.py create mode 100644 src/compas/artists/meshartist.py create mode 100644 src/compas/artists/networkartist.py create mode 100644 src/compas/artists/primitiveartist.py create mode 100644 src/compas/artists/robotmodelartist.py create mode 100644 src/compas/artists/shapeartist.py create mode 100644 src/compas/artists/volmeshartist.py 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/artists/meshartist.py b/src/compas/artists/meshartist.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/compas/artists/networkartist.py b/src/compas/artists/networkartist.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/compas/artists/primitiveartist.py b/src/compas/artists/primitiveartist.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/compas/artists/robotmodelartist.py b/src/compas/artists/robotmodelartist.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/compas/artists/shapeartist.py b/src/compas/artists/shapeartist.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/compas/artists/volmeshartist.py b/src/compas/artists/volmeshartist.py new file mode 100644 index 000000000000..e69de29bb2d1 From 25dcfb8b50db5573aec36c1edc48b1ffff5cf30e Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 21:17:52 +0200 Subject: [PATCH 12/71] the base mesh implementation --- src/compas/artists/meshartist.py | 232 +++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index e69de29bb2d1..d981c56beb5c 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -0,0 +1,232 @@ +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. + + 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. + + 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) + default_vertexcolor = (255, 255, 255) + default_edgecolor = (0, 0, 0) + default_facecolor = (0, 0, 0) + + def __init__(self, mesh): + self._mesh = None + self._vertices = None + self._edges = None + self._faces = None + self._color = None + self._vertex_xyz = None + self._vertex_color = None + self._edge_color = None + self._face_color = None + self._vertex_text = None + self._edge_text = None + self._face_text = None + self.mesh = mesh + self.join_faces = False + + @property + def mesh(self): + return self.item + + @mesh.setter + def mesh(self, mesh): + self.item = 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 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 + + @property + def vertex_color(self): + if not self._vertex_color: + 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 edge_color(self): + if not self._edge_color: + 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 face_color(self): + if not self._face_color: + 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 vertex_text(self): + if not self._vertex_text: + 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 edge_text(self): + if not self._edge_text: + 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 face_text(self): + if not self._face_text: + 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): + raise NotImplementedError + + @abstractmethod + def draw_edges(self, edges=None, color=None, text=None): + raise NotImplementedError + + @abstractmethod + def draw_faces(self, faces=None, color=None, text=None): + raise NotImplementedError From 618f3f6566e9ce414e836f81b42d2ad99eb8ab60 Mon Sep 17 00:00:00 2001 From: brgcode Date: Sun, 19 Sep 2021 21:44:41 +0200 Subject: [PATCH 13/71] docstrings --- src/compas/artists/meshartist.py | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index d981c56beb5c..1ab366df6da5 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -221,12 +221,57 @@ def face_text(self, 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 From b8c688626c46824a0b1e1b7619f1c248e549c224 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:30:11 +0200 Subject: [PATCH 14/71] clean up --- src/compas_rhino/artists/_primitiveartist.py | 5 +---- src/compas_rhino/artists/_shapeartist.py | 4 +--- src/compas_rhino/artists/boxartist.py | 2 +- src/compas_rhino/artists/capsuleartist.py | 2 +- src/compas_rhino/artists/circleartist.py | 5 +---- src/compas_rhino/artists/coneartist.py | 2 +- src/compas_rhino/artists/cylinderartist.py | 2 +- src/compas_rhino/artists/frameartist.py | 5 +---- src/compas_rhino/artists/lineartist.py | 5 +---- src/compas_rhino/artists/planeartist.py | 6 +----- src/compas_rhino/artists/pointartist.py | 5 +---- src/compas_rhino/artists/polygonartist.py | 5 +---- src/compas_rhino/artists/polyhedronartist.py | 2 +- src/compas_rhino/artists/polylineartist.py | 5 +---- src/compas_rhino/artists/robotmodelartist.py | 4 ---- src/compas_rhino/artists/sphereartist.py | 2 +- src/compas_rhino/artists/torusartist.py | 2 +- src/compas_rhino/artists/vectorartist.py | 5 +---- src/compas_rhino/artists/volmeshartist.py | 6 +----- 19 files changed, 18 insertions(+), 56 deletions(-) diff --git a/src/compas_rhino/artists/_primitiveartist.py b/src/compas_rhino/artists/_primitiveartist.py index 590b2f25959b..dd83874ecaba 100644 --- a/src/compas_rhino/artists/_primitiveartist.py +++ b/src/compas_rhino/artists/_primitiveartist.py @@ -3,10 +3,7 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._artist import RhinoArtist - - -__all__ = ["PrimitiveArtist"] +from ._artist import RhinoArtist class PrimitiveArtist(RhinoArtist): diff --git a/src/compas_rhino/artists/_shapeartist.py b/src/compas_rhino/artists/_shapeartist.py index cb6494fdd17b..880a9474307d 100644 --- a/src/compas_rhino/artists/_shapeartist.py +++ b/src/compas_rhino/artists/_shapeartist.py @@ -4,9 +4,7 @@ import compas_rhino from compas.datastructures import Mesh -from compas_rhino.artists._artist import RhinoArtist - -__all__ = ['ShapeArtist'] +from ._artist import RhinoArtist class ShapeArtist(RhinoArtist): diff --git a/src/compas_rhino/artists/boxartist.py b/src/compas_rhino/artists/boxartist.py index 2b35035cb9aa..0cd49c690d5d 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -3,7 +3,7 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from ._shapeartist import ShapeArtist class BoxArtist(ShapeArtist): diff --git a/src/compas_rhino/artists/capsuleartist.py b/src/compas_rhino/artists/capsuleartist.py index 5ae44cb0ba06..4b1f80dd99bd 100644 --- a/src/compas_rhino/artists/capsuleartist.py +++ b/src/compas_rhino/artists/capsuleartist.py @@ -4,7 +4,7 @@ from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from ._shapeartist import ShapeArtist class CapsuleArtist(ShapeArtist): diff --git a/src/compas_rhino/artists/circleartist.py b/src/compas_rhino/artists/circleartist.py index 8076f738e0bc..6bca0810f270 100644 --- a/src/compas_rhino/artists/circleartist.py +++ b/src/compas_rhino/artists/circleartist.py @@ -4,10 +4,7 @@ import compas_rhino from compas.geometry import add_vectors -from compas_rhino.artists._primitiveartist import PrimitiveArtist - - -__all__ = ['CircleArtist'] +from ._primitiveartist import PrimitiveArtist class CircleArtist(PrimitiveArtist): diff --git a/src/compas_rhino/artists/coneartist.py b/src/compas_rhino/artists/coneartist.py index 797a06e776a1..bffd3615f736 100644 --- a/src/compas_rhino/artists/coneartist.py +++ b/src/compas_rhino/artists/coneartist.py @@ -4,7 +4,7 @@ from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from ._shapeartist import ShapeArtist class ConeArtist(ShapeArtist): diff --git a/src/compas_rhino/artists/cylinderartist.py b/src/compas_rhino/artists/cylinderartist.py index 06132f49be92..85b8e853cf83 100644 --- a/src/compas_rhino/artists/cylinderartist.py +++ b/src/compas_rhino/artists/cylinderartist.py @@ -4,7 +4,7 @@ from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from ._shapeartist import ShapeArtist class CylinderArtist(ShapeArtist): diff --git a/src/compas_rhino/artists/frameartist.py b/src/compas_rhino/artists/frameartist.py index 2cc5481f1752..497bfb508ec4 100644 --- a/src/compas_rhino/artists/frameartist.py +++ b/src/compas_rhino/artists/frameartist.py @@ -3,10 +3,7 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist - - -__all__ = ['FrameArtist'] +from ._primitiveartist import PrimitiveArtist class FrameArtist(PrimitiveArtist): diff --git a/src/compas_rhino/artists/lineartist.py b/src/compas_rhino/artists/lineartist.py index 87480e1a4966..0f782d60c6e2 100644 --- a/src/compas_rhino/artists/lineartist.py +++ b/src/compas_rhino/artists/lineartist.py @@ -3,10 +3,7 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist - - -__all__ = ['LineArtist'] +from ._primitiveartist import PrimitiveArtist class LineArtist(PrimitiveArtist): diff --git a/src/compas_rhino/artists/planeartist.py b/src/compas_rhino/artists/planeartist.py index ea66aa8b7669..324d637cdd22 100644 --- a/src/compas_rhino/artists/planeartist.py +++ b/src/compas_rhino/artists/planeartist.py @@ -2,11 +2,7 @@ from __future__ import absolute_import from __future__ import division -# import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist - - -__all__ = ['PlaneArtist'] +from ._primitiveartist import PrimitiveArtist class PlaneArtist(PrimitiveArtist): diff --git a/src/compas_rhino/artists/pointartist.py b/src/compas_rhino/artists/pointartist.py index d1d32f128190..5d40327c0d01 100644 --- a/src/compas_rhino/artists/pointartist.py +++ b/src/compas_rhino/artists/pointartist.py @@ -3,10 +3,7 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist - - -__all__ = ['PointArtist'] +from ._primitiveartist import PrimitiveArtist class PointArtist(PrimitiveArtist): diff --git a/src/compas_rhino/artists/polygonartist.py b/src/compas_rhino/artists/polygonartist.py index 6d8e353cb5bc..241c8bd28788 100644 --- a/src/compas_rhino/artists/polygonartist.py +++ b/src/compas_rhino/artists/polygonartist.py @@ -3,10 +3,7 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist - - -__all__ = ['PolygonArtist'] +from ._primitiveartist import PrimitiveArtist class PolygonArtist(PrimitiveArtist): diff --git a/src/compas_rhino/artists/polyhedronartist.py b/src/compas_rhino/artists/polyhedronartist.py index b6b269a3f5a6..a627ac1097fe 100644 --- a/src/compas_rhino/artists/polyhedronartist.py +++ b/src/compas_rhino/artists/polyhedronartist.py @@ -3,7 +3,7 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from ._shapeartist import ShapeArtist class PolyhedronArtist(ShapeArtist): diff --git a/src/compas_rhino/artists/polylineartist.py b/src/compas_rhino/artists/polylineartist.py index ee989993e988..c8b476d4763f 100644 --- a/src/compas_rhino/artists/polylineartist.py +++ b/src/compas_rhino/artists/polylineartist.py @@ -3,10 +3,7 @@ from __future__ import division import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist - - -__all__ = ['PolylineArtist'] +from ._primitiveartist import PrimitiveArtist class PolylineArtist(PrimitiveArtist): diff --git a/src/compas_rhino/artists/robotmodelartist.py b/src/compas_rhino/artists/robotmodelartist.py index 5473e93f99f5..bf82717c85df 100644 --- a/src/compas_rhino/artists/robotmodelartist.py +++ b/src/compas_rhino/artists/robotmodelartist.py @@ -19,10 +19,6 @@ from compas_rhino.artists import RhinoArtist from compas_rhino.geometry.transformations import xform_from_transformation -__all__ = [ - 'RobotModelArtist', -] - class RobotModelArtist(BaseRobotModelArtist, RhinoArtist): """Visualizer for robots inside a Rhino environment. diff --git a/src/compas_rhino/artists/sphereartist.py b/src/compas_rhino/artists/sphereartist.py index 9068cf30796a..bda7c1db1e97 100644 --- a/src/compas_rhino/artists/sphereartist.py +++ b/src/compas_rhino/artists/sphereartist.py @@ -4,7 +4,7 @@ from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from ._shapeartist import ShapeArtist class SphereArtist(ShapeArtist): diff --git a/src/compas_rhino/artists/torusartist.py b/src/compas_rhino/artists/torusartist.py index fe99c8364f2e..9f8dcdc7a017 100644 --- a/src/compas_rhino/artists/torusartist.py +++ b/src/compas_rhino/artists/torusartist.py @@ -4,7 +4,7 @@ from compas.utilities import pairwise import compas_rhino -from compas_rhino.artists._shapeartist import ShapeArtist +from ._shapeartist import ShapeArtist class TorusArtist(ShapeArtist): diff --git a/src/compas_rhino/artists/vectorartist.py b/src/compas_rhino/artists/vectorartist.py index b95a4a77a2cd..39c4c0ea2b89 100644 --- a/src/compas_rhino/artists/vectorartist.py +++ b/src/compas_rhino/artists/vectorartist.py @@ -4,10 +4,7 @@ from compas.geometry import Point import compas_rhino -from compas_rhino.artists._primitiveartist import PrimitiveArtist - - -__all__ = ['VectorArtist'] +from ._primitiveartist import PrimitiveArtist class VectorArtist(PrimitiveArtist): diff --git a/src/compas_rhino/artists/volmeshartist.py b/src/compas_rhino/artists/volmeshartist.py index 74468193e128..af012b69bb81 100644 --- a/src/compas_rhino/artists/volmeshartist.py +++ b/src/compas_rhino/artists/volmeshartist.py @@ -5,18 +5,14 @@ from functools import partial import compas_rhino -from compas_rhino.artists._artist import RhinoArtist - from compas.utilities import color_to_colordict from compas.geometry import centroid_points +from ._artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['VolMeshArtist'] - - class VolMeshArtist(RhinoArtist): """Artist for drawing volmesh data structures. From 2c435edb0c71b1872dee554b92b87afd0cd52251 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:30:39 +0200 Subject: [PATCH 15/71] add redraw shortcut --- src/compas_rhino/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/compas_rhino/__init__.py b/src/compas_rhino/__init__.py index 80424be8e041..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']) From 33c780cbddfdf8a467c69d24c6827b28f424b894 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:30:57 +0200 Subject: [PATCH 16/71] remove abstract redraw --- src/compas/artists/artist.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/compas/artists/artist.py b/src/compas/artists/artist.py index eb74b86b9829..2b649ce85d1e 100644 --- a/src/compas/artists/artist.py +++ b/src/compas/artists/artist.py @@ -48,10 +48,6 @@ def build(item, **kwargs): def draw(self): raise NotImplementedError - @abstractmethod - def redraw(self): - raise NotImplementedError - @staticmethod def draw_collection(collection): raise NotImplementedError From b64891ea736ac3635afe0f4a24944c71ebedeb8e Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:31:12 +0200 Subject: [PATCH 17/71] base mesh artist --- src/compas/artists/meshartist.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index 1ab366df6da5..1dfa9e6d1105 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -1,6 +1,7 @@ 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 @@ -10,6 +11,11 @@ class MeshArtist(Artist): """Base class for all mesh artists. + Parameters + ---------- + mesh : :class:`compas.datastructures.Mesh` + A COMPAS mesh. + Class Attributes ---------------- default_color : tuple @@ -60,6 +66,7 @@ class MeshArtist(Artist): default_facecolor = (0, 0, 0) def __init__(self, mesh): + super(MeshArtist, self).__init__() self._mesh = None self._vertices = None self._edges = None @@ -73,15 +80,14 @@ def __init__(self, mesh): self._edge_text = None self._face_text = None self.mesh = mesh - self.join_faces = False @property def mesh(self): - return self.item + return self._mesh @mesh.setter def mesh(self, mesh): - self.item = mesh + self._mesh = mesh self._vertex_xyz = None @property From 1b34a18b2bb04ce82b65618337c6cf8d4e800348 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:31:21 +0200 Subject: [PATCH 18/71] base network artists --- src/compas/artists/networkartist.py | 202 ++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/src/compas/artists/networkartist.py b/src/compas/artists/networkartist.py index e69de29bb2d1..b0aee810d5fd 100644 --- a/src/compas/artists/networkartist.py +++ b/src/compas/artists/networkartist.py @@ -0,0 +1,202 @@ +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 = (255, 255, 255) + default_edgecolor = (0, 0, 0) + + def __init__(self, network): + super(NetworkArtist, self).__init__() + 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.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 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 + + @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 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 From 4bc62cfdc5ec990b3de339023f59c58425ea2257 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:31:36 +0200 Subject: [PATCH 19/71] simplify base rhino artist --- src/compas_rhino/artists/_artist.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/compas_rhino/artists/_artist.py b/src/compas_rhino/artists/_artist.py index 806553f25b2a..d8635724794a 100644 --- a/src/compas_rhino/artists/_artist.py +++ b/src/compas_rhino/artists/_artist.py @@ -8,23 +8,8 @@ class RhinoArtist(Artist): """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 = [] - - 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 = [] + def clear_layer(self): + if self.layer: + compas_rhino.clear_layer(self.layer) From 76fd2acbb0a6511acb6d0936fe7916c6bc691ed0 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:31:50 +0200 Subject: [PATCH 20/71] rhino implementations of mesh and network --- src/compas_rhino/artists/meshartist.py | 133 +++++----------------- src/compas_rhino/artists/networkartist.py | 94 +++++---------- 2 files changed, 56 insertions(+), 171 deletions(-) diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index a61ce8ae2087..1a04f3c9c1e6 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -5,8 +5,6 @@ from functools import partial import compas_rhino -from compas_rhino.artists._artist import RhinoArtist - from compas.utilities import color_to_colordict from compas.utilities import pairwise from compas.geometry import add_vectors @@ -14,14 +12,13 @@ from compas.geometry import centroid_polygon from compas.geometry import centroid_points +from compas.artists.meshartist import MeshArtist +from ._artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['MeshArtist'] - - -class MeshArtist(RhinoArtist): +class MeshArtist(MeshArtist, RhinoArtist): """Artists for drawing mesh data structures. Parameters @@ -30,112 +27,29 @@ class MeshArtist(RhinoArtist): 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 + super(MeshArtist, self).__init__(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 - - # ========================================================================== - # clear - # ========================================================================== def clear_by_name(self): """Clear all objects in the "namespace" of the associated mesh.""" 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) - # ========================================================================== # draw # ========================================================================== - def draw(self): - """Draw the mesh using the chosen visualisation settings. - - Returns - ------- - list - The GUIDs of the created Rhino objects. - - """ - guids = self.draw_vertices() - guids += self.draw_faces() - guids += self.draw_edges() - return guids - - def draw_mesh(self, color=(0, 0, 0), disjoint=False): + def draw(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 +64,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()] @@ -183,7 +98,7 @@ def draw_vertices(self, vertices=None, color=None): 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,15 +106,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.mesh.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]}) + 'color': self.vertex_color.get(vertex, self.default_vertexcolor) + }) return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) def draw_faces(self, faces=None, color=None, join_faces=False): @@ -212,7 +128,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,23 +139,23 @@ def draw_faces(self, faces=None, color=None, join_faces=False): The GUIDs of the created Rhino objects. """ + self.face_color = color faces = faces or list(self.mesh.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: - compas_rhino.rs.ObjectColor(guid, color) + compas_rhino.rs.ObjectColor(guid, color) return [guid] def draw_edges(self, edges=None, color=None): @@ -252,7 +168,7 @@ def draw_edges(self, edges=None, color=None): 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)``. + The default color is the value of ``~MeshArtist.default_edgecolor``. Returns ------- @@ -260,16 +176,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.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)}) + 'color': self.edge_color.get(edge, self.default_edgecolor), + 'name': "{}.edge.{}-{}".format(self.mesh.name, *edge) + }) return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) # ========================================================================== @@ -309,7 +226,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): @@ -345,7 +263,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) # ========================================================================== diff --git a/src/compas_rhino/artists/networkartist.py b/src/compas_rhino/artists/networkartist.py index 441e19e85ca8..d7999da85e7a 100644 --- a/src/compas_rhino/artists/networkartist.py +++ b/src/compas_rhino/artists/networkartist.py @@ -4,17 +4,15 @@ from functools import partial import compas_rhino -from compas_rhino.artists._artist import RhinoArtist + from compas.geometry import centroid_points from compas.utilities import color_to_colordict +from ._artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -__all__ = ['NetworkArtist'] - - class NetworkArtist(RhinoArtist): """Artist for drawing network data structures. @@ -24,73 +22,39 @@ class NetworkArtist(RhinoArtist): 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. - """ def __init__(self, network, layer=None): - super(NetworkArtist, self).__init__() - self._network = None - self._node_xyz = None - self.network = network + super(NetworkArtist, self).__init__(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 - - # ========================================================================== - # clear - # ========================================================================== def clear_by_name(self): """Clear all objects in the "namespace" of the associated network.""" 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) - # ========================================================================== # 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 @@ -109,9 +73,9 @@ 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 ------- @@ -119,15 +83,16 @@ def draw_nodes(self, nodes=None, color=None): 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) 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,9 +103,9 @@ 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 ------- @@ -148,16 +113,17 @@ def draw_edges(self, edges=None, color=None): 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) 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) # ========================================================================== From 5f6f54080bae71129d298c0675cec9864f6be8e5 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:36:46 +0200 Subject: [PATCH 21/71] clean up merge --- src/compas_blender/artists/frameartist.py | 2 +- src/compas_blender/artists/meshartist.py | 1 - src/compas_blender/artists/networkartist.py | 2 -- src/compas_blender/artists/robotmodelartist.py | 2 +- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/compas_blender/artists/frameartist.py b/src/compas_blender/artists/frameartist.py index f8dfc59e5689..fdcae61c9bd5 100644 --- a/src/compas_blender/artists/frameartist.py +++ b/src/compas_blender/artists/frameartist.py @@ -62,7 +62,7 @@ def __init__(self, frame: Frame, collection: Optional[bpy.types.Collection] = None, scale: float = 1.0): - super(FrameArtist, self).__init__() + super().__init__() self.collection = collection self.frame = frame self.scale = scale or 1.0 diff --git a/src/compas_blender/artists/meshartist.py b/src/compas_blender/artists/meshartist.py index 4b5c86f35a66..0bb02effca3e 100644 --- a/src/compas_blender/artists/meshartist.py +++ b/src/compas_blender/artists/meshartist.py @@ -1,4 +1,3 @@ -# from __future__ import annotations from typing import Dict from typing import List from typing import Optional diff --git a/src/compas_blender/artists/networkartist.py b/src/compas_blender/artists/networkartist.py index 164918f0e2d9..a724019ca7cb 100644 --- a/src/compas_blender/artists/networkartist.py +++ b/src/compas_blender/artists/networkartist.py @@ -1,4 +1,3 @@ -# from __future__ import annotations from typing import Dict from typing import List from typing import Optional @@ -9,7 +8,6 @@ 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 diff --git a/src/compas_blender/artists/robotmodelartist.py b/src/compas_blender/artists/robotmodelartist.py index 37899b92d7ac..f64ab8953767 100644 --- a/src/compas_blender/artists/robotmodelartist.py +++ b/src/compas_blender/artists/robotmodelartist.py @@ -23,7 +23,7 @@ def __init__(self, model: RobotModel, collection: bpy.types.Collection = None): self.collection = collection or model.name - super(RobotModelArtist, self).__init__(model) + super().__init__(model) @property def collection(self) -> bpy.types.Collection: From 4618a504683b7c190222ba7e7ae4ecf2e876eb24 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:49:29 +0200 Subject: [PATCH 22/71] base class for primitive artist --- src/compas/artists/primitiveartist.py | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/compas/artists/primitiveartist.py b/src/compas/artists/primitiveartist.py index e69de29bb2d1..cf8b9fcf4154 100644 --- a/src/compas/artists/primitiveartist.py +++ 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): + 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 From 5673b21eacf81d4379b55e701f84fa54afa92a00 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 10:49:40 +0200 Subject: [PATCH 23/71] base class for shape artist --- src/compas/artists/shapeartist.py | 88 +++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/compas/artists/shapeartist.py b/src/compas/artists/shapeartist.py index e69de29bb2d1..8cb6e5e2b3ec 100644 --- a/src/compas/artists/shapeartist.py +++ b/src/compas/artists/shapeartist.py @@ -0,0 +1,88 @@ +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 = (255, 255, 255) + + def __init__(self, shape, color=None): + super(ShapeArtist, self).__init__() + self._u = None + self._v = None + self._shape = None + self._color = None + self.shape = shape + self.color = color + + @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 > 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 > 3: + self._v = v From 66e88661685e75a67df23463161b1e5b9c5c9664 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 11:02:29 +0200 Subject: [PATCH 24/71] base artists for volmeshes --- src/compas/artists/volmeshartist.py | 348 ++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) diff --git a/src/compas/artists/volmeshartist.py b/src/compas/artists/volmeshartist.py index e69de29bb2d1..3e25efbd61db 100644 --- a/src/compas/artists/volmeshartist.py +++ b/src/compas/artists/volmeshartist.py @@ -0,0 +1,348 @@ +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 = (255, 255, 255) + default_edgecolor = (0, 0, 0) + default_facecolor = (210, 210, 210) + default_cellcolor = (255, 0, 0) + + def __init__(self, volmesh): + super(VolMeshArtist, self).__init__() + 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 From 9ec87ca1cced84177f9e5e668b1eef3614577831 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 11:53:43 +0200 Subject: [PATCH 25/71] unnecessary --- src/compas_rhino/artists/_primitiveartist.py | 52 ---------------- src/compas_rhino/artists/_shapeartist.py | 65 -------------------- 2 files changed, 117 deletions(-) delete mode 100644 src/compas_rhino/artists/_primitiveartist.py delete mode 100644 src/compas_rhino/artists/_shapeartist.py diff --git a/src/compas_rhino/artists/_primitiveartist.py b/src/compas_rhino/artists/_primitiveartist.py deleted file mode 100644 index dd83874ecaba..000000000000 --- a/src/compas_rhino/artists/_primitiveartist.py +++ /dev/null @@ -1,52 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import compas_rhino -from ._artist import RhinoArtist - - -class PrimitiveArtist(RhinoArtist): - """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 880a9474307d..000000000000 --- a/src/compas_rhino/artists/_shapeartist.py +++ /dev/null @@ -1,65 +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 ._artist import RhinoArtist - - -class ShapeArtist(RhinoArtist): - """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) From 46af2e29e77aa48812c410418b030fff60371925 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 11:53:52 +0200 Subject: [PATCH 26/71] pull up --- src/compas/artists/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/compas/artists/__init__.py b/src/compas/artists/__init__.py index 81de4740bb1d..5b1d6ca4b7b6 100644 --- a/src/compas/artists/__init__.py +++ b/src/compas/artists/__init__.py @@ -13,6 +13,11 @@ :nosignatures: Artist + MeshArtist + NetworkArtist + PrimitiveArtist + ShapeArtist + VolMeshArtist Exceptions @@ -31,8 +36,18 @@ from .exceptions import DataArtistNotRegistered from .artist import Artist +from .meshartist import MeshArtist +from .networkartist import NetworkArtist +from .primitiveartist import PrimitiveArtist +from .shapeartist import ShapeArtist +from .volmeshartist import VolMeshArtist __all__ = [ 'DataArtistNotRegistered', 'Artist', + 'MeshArtist', + 'NetworkArtist', + 'PrimitiveArtist', + 'ShapeArtist', + 'VolMeshArtist', ] From 11d296a8555c120e6bb38611865a6032e8f00865 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 11:54:01 +0200 Subject: [PATCH 27/71] rebase box artist --- src/compas_rhino/artists/boxartist.py | 41 +++++++++------------------ 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/compas_rhino/artists/boxartist.py b/src/compas_rhino/artists/boxartist.py index 0cd49c690d5d..85bd44e9ab68 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -3,44 +3,30 @@ from __future__ import division import compas_rhino -from ._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` 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) + def __init__(self, box, layer=None): + super(BoxArtist, self).__init__(box) + self.layer = layer - compas_rhino.clear_layer("Test::BoxArtist") + @property + def box(self): + return self.shape - 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() - """ + @box.setter + def box(self, box): + self.shape = box def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the box associated with the artist. @@ -78,5 +64,4 @@ def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_face 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 From 28a5bde7c44b05b54e1e323ec5fd1c96e2d70f3b Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 13:58:57 +0200 Subject: [PATCH 28/71] rebasing on base artists --- src/compas_rhino/artists/__init__.py | 6 - src/compas_rhino/artists/boxartist.py | 20 +-- src/compas_rhino/artists/capsuleartist.py | 52 +++---- src/compas_rhino/artists/circleartist.py | 48 ++----- src/compas_rhino/artists/coneartist.py | 48 ++----- src/compas_rhino/artists/cylinderartist.py | 49 ++----- src/compas_rhino/artists/frameartist.py | 37 +---- src/compas_rhino/artists/lineartist.py | 47 ++----- src/compas_rhino/artists/meshartist.py | 44 +++++- src/compas_rhino/artists/networkartist.py | 7 +- src/compas_rhino/artists/planeartist.py | 18 +-- src/compas_rhino/artists/pointartist.py | 40 ++---- src/compas_rhino/artists/polygonartist.py | 48 ++----- src/compas_rhino/artists/polyhedronartist.py | 43 ++---- src/compas_rhino/artists/polylineartist.py | 44 ++---- src/compas_rhino/artists/sphereartist.py | 52 +++---- src/compas_rhino/artists/torusartist.py | 52 +++---- src/compas_rhino/artists/vectorartist.py | 44 ++---- src/compas_rhino/artists/volmeshartist.py | 139 +++++++++---------- 19 files changed, 292 insertions(+), 546 deletions(-) diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 8e826a6080a9..8ef63494ed1b 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -69,8 +69,6 @@ :nosignatures: RhinoArtist - PrimitiveArtist - ShapeArtist """ from __future__ import absolute_import @@ -105,8 +103,6 @@ from compas.robots import RobotModel from ._artist import RhinoArtist -from ._primitiveartist import PrimitiveArtist -from ._shapeartist import ShapeArtist from .circleartist import CircleArtist from .frameartist import FrameArtist @@ -171,8 +167,6 @@ def new_artist_rhino(cls, *args, **kwargs): __all__ = [ 'RhinoArtist', - 'PrimitiveArtist', - 'ShapeArtist', 'CircleArtist', 'FrameArtist', 'LineArtist', diff --git a/src/compas_rhino/artists/boxartist.py b/src/compas_rhino/artists/boxartist.py index 85bd44e9ab68..984ff431cae8 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -12,22 +12,16 @@ class BoxArtist(RhinoArtist, ShapeArtist): Parameters ---------- - shape : :class:`compas.geometry.Box` + box : :class:`compas.geometry.Box` A COMPAS box. + layer : str, optional + The layer that should contain the drawing. """ def __init__(self, box, layer=None): super(BoxArtist, self).__init__(box) self.layer = layer - @property - def box(self): - return self.shape - - @box.setter - def box(self, box): - self.shape = box - def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the box associated with the artist. @@ -50,18 +44,18 @@ def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_face 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] + points = [{'pos': point, 'color': self.color, 'name': self.shape.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] + lines = [{'start': vertices[i], 'end': vertices[j], 'color': self.color, 'name': self.shape.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) + guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.shape.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] + polygons = [{'points': [vertices[index] for index in face], 'color': self.color, 'name': self.shape.name} for face in faces] guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) return guids diff --git a/src/compas_rhino/artists/capsuleartist.py b/src/compas_rhino/artists/capsuleartist.py index 4b1f80dd99bd..79decb870339 100644 --- a/src/compas_rhino/artists/capsuleartist.py +++ b/src/compas_rhino/artists/capsuleartist.py @@ -4,57 +4,36 @@ from compas.utilities import pairwise import compas_rhino -from ._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): + super(CapsuleArtist, self).__init__(capsule) + self.layer = layer + + def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the capsule associated with the artist. Parameters ---------- 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``. + Default is ``~CapsuleArtist.v``. show_vertices : bool, optional Default is ``False``. show_edges : bool, optional @@ -69,6 +48,8 @@ def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=Tru list The GUIDs of the objects created in Rhino. """ + 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 = [] @@ -87,10 +68,9 @@ def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=Tru 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) + guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.shape.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 diff --git a/src/compas_rhino/artists/circleartist.py b/src/compas_rhino/artists/circleartist.py index 6bca0810f270..d78c6229f8f9 100644 --- a/src/compas_rhino/artists/circleartist.py +++ b/src/compas_rhino/artists/circleartist.py @@ -4,46 +4,25 @@ import compas_rhino from compas.geometry import add_vectors -from ._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from ._artist import RhinoArtist -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): + super(CircleArtist, self).__init__(circle) + self.layer = layer + def draw(self, show_point=False, show_normal=False): """Draw the circle. @@ -65,12 +44,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 bffd3615f736..c954311ecb65 100644 --- a/src/compas_rhino/artists/coneartist.py +++ b/src/compas_rhino/artists/coneartist.py @@ -4,56 +4,34 @@ from compas.utilities import pairwise import compas_rhino -from ._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): + super(ConeArtist, self).__init__(cone) + self.layer = layer + + def draw(self, u=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the cone associated with the artist. Parameters ---------- u : int, optional Number of faces in the "u" direction. - Default is ``10``. + Default is ``~ConeArtist.u``. show_vertices : bool, optional Default is ``False``. show_edges : bool, optional @@ -68,6 +46,7 @@ def draw(self, u=10, show_vertices=False, show_edges=False, show_faces=True, joi list The GUIDs of the objects created in Rhino. """ + u = u or self.u vertices, faces = self.shape.to_vertices_and_faces(u=u) vertices = [list(vertex) for vertex in vertices] guids = [] @@ -86,10 +65,9 @@ def draw(self, u=10, show_vertices=False, show_edges=False, show_faces=True, joi 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) + guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.shape.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 diff --git a/src/compas_rhino/artists/cylinderartist.py b/src/compas_rhino/artists/cylinderartist.py index 85b8e853cf83..1c6296180ae2 100644 --- a/src/compas_rhino/artists/cylinderartist.py +++ b/src/compas_rhino/artists/cylinderartist.py @@ -4,54 +4,33 @@ from compas.utilities import pairwise import compas_rhino -from ._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): + super(CylinderArtist, self).__init__(cylinder) + self.layer = layer + + def draw(self, u=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the cylinder associated with the artist. Parameters ---------- u : int, optional Number of faces in the "u" direction. - Default is ``10``. + Default is ``~CylinderArtist.u``. show_vertices : bool, optional Default is ``False``. show_edges : bool, optional @@ -66,6 +45,7 @@ def draw(self, u=10, show_vertices=False, show_edges=False, show_faces=True, joi list The GUIDs of the objects created in Rhino. """ + u = u or self.u vertices, faces = self.shape.to_vertices_and_faces(u=u) vertices = [list(vertex) for vertex in vertices] guids = [] @@ -84,10 +64,9 @@ def draw(self, u=10, show_vertices=False, show_edges=False, show_faces=True, joi 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) + guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.shape.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 diff --git a/src/compas_rhino/artists/frameartist.py b/src/compas_rhino/artists/frameartist.py index 497bfb508ec4..c13c4ab26306 100644 --- a/src/compas_rhino/artists/frameartist.py +++ b/src/compas_rhino/artists/frameartist.py @@ -3,10 +3,11 @@ from __future__ import division import compas_rhino -from ._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from ._artist import RhinoArtist -class FrameArtist(PrimitiveArtist): +class FrameArtist(RhinoArtist, PrimitiveArtist): """Artist for drawing frames. Parameters @@ -15,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 ---------- @@ -33,32 +32,11 @@ 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) + super(FrameArtist, self).__init__(frame) + self.layer = layer self.scale = scale or 1.0 self.color_origin = (0, 0, 0) self.color_xaxis = (255, 0, 0) @@ -86,5 +64,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 0f782d60c6e2..cc7c119ef986 100644 --- a/src/compas_rhino/artists/lineartist.py +++ b/src/compas_rhino/artists/lineartist.py @@ -3,45 +3,25 @@ from __future__ import division import compas_rhino -from ._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from ._artist import RhinoArtist -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): + super(LineArtist, self).__init__(line) + self.layer = layer + def draw(self, show_points=False): """Draw the line. @@ -62,11 +42,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 1a04f3c9c1e6..cd7a2a196763 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -12,13 +12,13 @@ from compas.geometry import centroid_polygon from compas.geometry import centroid_points -from compas.artists.meshartist import MeshArtist +from compas.artists import MeshArtist from ._artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) -class MeshArtist(MeshArtist, RhinoArtist): +class MeshArtist(RhinoArtist, MeshArtist): """Artists for drawing mesh data structures. Parameters @@ -42,7 +42,45 @@ def clear_by_name(self): # draw # ========================================================================== - def draw(self, color=None, disjoint=False): + 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. + + """ + guids = self.draw_vertices(vertices=vertices, color=vertexcolor) + guids += self.draw_edges(edges=edges, color=edgecolor) + guids += self.draw_faces(faces=faces, color=facecolor, join_faces=join_faces) + return guids + + def draw_mesh(self, color=None, disjoint=False): """Draw the mesh as a consolidated RhinoMesh. Parameters diff --git a/src/compas_rhino/artists/networkartist.py b/src/compas_rhino/artists/networkartist.py index d7999da85e7a..56dc61785365 100644 --- a/src/compas_rhino/artists/networkartist.py +++ b/src/compas_rhino/artists/networkartist.py @@ -8,12 +8,13 @@ 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) -class NetworkArtist(RhinoArtist): +class NetworkArtist(RhinoArtist, NetworkArtist): """Artist for drawing network data structures. Parameters @@ -61,8 +62,8 @@ def draw(self, nodes=None, edges=None, nodecolor=None, edgecolor=None): The GUIDs of the created Rhino objects. """ - guids = self.draw_nodes() - guids += self.draw_edges() + 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): diff --git a/src/compas_rhino/artists/planeartist.py b/src/compas_rhino/artists/planeartist.py index 324d637cdd22..f520ce26b4de 100644 --- a/src/compas_rhino/artists/planeartist.py +++ b/src/compas_rhino/artists/planeartist.py @@ -2,23 +2,25 @@ from __future__ import absolute_import from __future__ import division -from ._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from ._artist import RhinoArtist -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): + super(PlaneArtist, self).__init__(plane) + self.layer = layer + def draw(self): """Draw the plane. diff --git a/src/compas_rhino/artists/pointartist.py b/src/compas_rhino/artists/pointartist.py index 5d40327c0d01..1049936bbdbe 100644 --- a/src/compas_rhino/artists/pointartist.py +++ b/src/compas_rhino/artists/pointartist.py @@ -3,42 +3,25 @@ from __future__ import division import compas_rhino -from ._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from ._artist import RhinoArtist -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): + super(PointArtist, self).__init__(point) + self.layer = layer + def draw(self): """Draw the point. @@ -48,7 +31,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 241c8bd28788..cd8be320c2b3 100644 --- a/src/compas_rhino/artists/polygonartist.py +++ b/src/compas_rhino/artists/polygonartist.py @@ -3,46 +3,25 @@ from __future__ import division import compas_rhino -from ._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from ._artist import RhinoArtist -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): + super(PolygonArtist, self).__init__(polygon) + self.layer = layer + def draw(self, show_points=False, show_edges=False, show_face=True): """Draw the polygon. @@ -63,13 +42,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 a627ac1097fe..b9a6f05de40c 100644 --- a/src/compas_rhino/artists/polyhedronartist.py +++ b/src/compas_rhino/artists/polyhedronartist.py @@ -3,45 +3,25 @@ from __future__ import division import compas_rhino -from ._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 __init__(self, polyhedron, layer=None): + super(PolyhedronArtist, self).__init__(polyhedron) + self.layer = layer + def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the polyhedron associated with the artist. @@ -73,10 +53,9 @@ def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_face 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) + guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.shape.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 diff --git a/src/compas_rhino/artists/polylineartist.py b/src/compas_rhino/artists/polylineartist.py index c8b476d4763f..1fcd9f6375b2 100644 --- a/src/compas_rhino/artists/polylineartist.py +++ b/src/compas_rhino/artists/polylineartist.py @@ -3,46 +3,23 @@ from __future__ import division import compas_rhino -from ._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from ._artist import RhinoArtist -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): + super(PolylineArtist, self).__init__(polyline) + self.layer = layer + def draw(self, show_points=False): """Draw the polyline. @@ -59,9 +36,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/sphereartist.py b/src/compas_rhino/artists/sphereartist.py index bda7c1db1e97..9bfcbc8f9832 100644 --- a/src/compas_rhino/artists/sphereartist.py +++ b/src/compas_rhino/artists/sphereartist.py @@ -4,57 +4,36 @@ from compas.utilities import pairwise import compas_rhino -from ._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): + super(SphereArtist, self).__init__(sphere) + self.layer = layer + + def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the sphere associated with the artist. Parameters ---------- 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``. + Default is ``~SphereArtist.v``. show_vertices : bool, optional Default is ``False``. show_edges : bool, optional @@ -69,6 +48,8 @@ def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=Tru list The GUIDs of the objects created in Rhino. """ + 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 = [] @@ -87,10 +68,9 @@ def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=Tru 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) + guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.shape.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 diff --git a/src/compas_rhino/artists/torusartist.py b/src/compas_rhino/artists/torusartist.py index 9f8dcdc7a017..317aa8959469 100644 --- a/src/compas_rhino/artists/torusartist.py +++ b/src/compas_rhino/artists/torusartist.py @@ -4,57 +4,36 @@ from compas.utilities import pairwise import compas_rhino -from ._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): + super(TorusArtist, self).__init__(torus) + self.layer = layer + + def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the torus associated with the artist. Parameters ---------- 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``. + Default is ``~TorusArtist.v``. show_vertices : bool, optional Default is ``False``. show_edges : bool, optional @@ -69,6 +48,8 @@ def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=Tru list The GUIDs of the objects created in Rhino. """ + 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 = [] @@ -87,10 +68,9 @@ def draw(self, u=10, v=10, show_vertices=False, show_edges=False, show_faces=Tru 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) + guid = compas_rhino.draw_mesh(vertices, faces, layer=self.layer, name=self.shape.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 diff --git a/src/compas_rhino/artists/vectorartist.py b/src/compas_rhino/artists/vectorartist.py index 39c4c0ea2b89..10debd0337c0 100644 --- a/src/compas_rhino/artists/vectorartist.py +++ b/src/compas_rhino/artists/vectorartist.py @@ -4,44 +4,25 @@ from compas.geometry import Point import compas_rhino -from ._primitiveartist import PrimitiveArtist +from compas.artists import PrimitiveArtist +from ._artist import RhinoArtist -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): + super(VectorArtist, self).__init__(vector) + self.layer = layer + def draw(self, point=None, show_point=False): """Draw the vector. @@ -68,9 +49,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 af012b69bb81..acf102fe2a72 100644 --- a/src/compas_rhino/artists/volmeshartist.py +++ b/src/compas_rhino/artists/volmeshartist.py @@ -8,12 +8,13 @@ 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) -class VolMeshArtist(RhinoArtist): +class VolMeshArtist(RhinoArtist, VolMeshArtist): """Artist for drawing volmesh data structures. Parameters @@ -22,80 +23,62 @@ class VolMeshArtist(RhinoArtist): 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 + super(VolMeshArtist, self).__init__(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 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. @@ -107,7 +90,7 @@ def draw_vertices(self, vertices=None, color=None): 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 ------- @@ -115,15 +98,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): @@ -136,7 +120,7 @@ def draw_edges(self, edges=None, color=None): 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 ------- @@ -144,16 +128,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): @@ -166,7 +151,7 @@ def draw_faces(self, faces=None, color=None): 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 ------- @@ -174,15 +159,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): @@ -195,7 +181,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 ------- @@ -204,9 +190,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 = [] @@ -214,12 +200,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 @@ -261,7 +248,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): @@ -296,7 +284,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): @@ -333,7 +322,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): @@ -370,5 +360,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) From ea2d08349356380d2d555e45c0d9b94c0ed66627 Mon Sep 17 00:00:00 2001 From: brgcode Date: Mon, 20 Sep 2021 14:00:36 +0200 Subject: [PATCH 29/71] remove underscore --- src/compas_rhino/artists/__init__.py | 2 +- src/compas_rhino/artists/{_artist.py => artist.py} | 0 src/compas_rhino/artists/boxartist.py | 2 +- src/compas_rhino/artists/capsuleartist.py | 2 +- src/compas_rhino/artists/circleartist.py | 2 +- src/compas_rhino/artists/coneartist.py | 2 +- src/compas_rhino/artists/cylinderartist.py | 2 +- src/compas_rhino/artists/frameartist.py | 2 +- src/compas_rhino/artists/lineartist.py | 2 +- src/compas_rhino/artists/meshartist.py | 2 +- src/compas_rhino/artists/networkartist.py | 2 +- src/compas_rhino/artists/planeartist.py | 2 +- src/compas_rhino/artists/pointartist.py | 2 +- src/compas_rhino/artists/polygonartist.py | 2 +- src/compas_rhino/artists/polyhedronartist.py | 2 +- src/compas_rhino/artists/polylineartist.py | 2 +- src/compas_rhino/artists/sphereartist.py | 2 +- src/compas_rhino/artists/torusartist.py | 2 +- src/compas_rhino/artists/vectorartist.py | 2 +- src/compas_rhino/artists/volmeshartist.py | 2 +- 20 files changed, 19 insertions(+), 19 deletions(-) rename src/compas_rhino/artists/{_artist.py => artist.py} (100%) diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 8ef63494ed1b..71f5d97fc777 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -102,7 +102,7 @@ from compas.robots import RobotModel -from ._artist import RhinoArtist +from .artist import RhinoArtist from .circleartist import CircleArtist from .frameartist import FrameArtist diff --git a/src/compas_rhino/artists/_artist.py b/src/compas_rhino/artists/artist.py similarity index 100% rename from src/compas_rhino/artists/_artist.py rename to src/compas_rhino/artists/artist.py diff --git a/src/compas_rhino/artists/boxartist.py b/src/compas_rhino/artists/boxartist.py index 984ff431cae8..dce28f12ab90 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -4,7 +4,7 @@ import compas_rhino from compas.artists import ShapeArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class BoxArtist(RhinoArtist, ShapeArtist): diff --git a/src/compas_rhino/artists/capsuleartist.py b/src/compas_rhino/artists/capsuleartist.py index 79decb870339..8d9ca0c95196 100644 --- a/src/compas_rhino/artists/capsuleartist.py +++ b/src/compas_rhino/artists/capsuleartist.py @@ -5,7 +5,7 @@ from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class CapsuleArtist(RhinoArtist, ShapeArtist): diff --git a/src/compas_rhino/artists/circleartist.py b/src/compas_rhino/artists/circleartist.py index d78c6229f8f9..4465c18ebfb6 100644 --- a/src/compas_rhino/artists/circleartist.py +++ b/src/compas_rhino/artists/circleartist.py @@ -5,7 +5,7 @@ import compas_rhino from compas.geometry import add_vectors from compas.artists import PrimitiveArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class CircleArtist(RhinoArtist, PrimitiveArtist): diff --git a/src/compas_rhino/artists/coneartist.py b/src/compas_rhino/artists/coneartist.py index c954311ecb65..eb2ea558620d 100644 --- a/src/compas_rhino/artists/coneartist.py +++ b/src/compas_rhino/artists/coneartist.py @@ -5,7 +5,7 @@ from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class ConeArtist(RhinoArtist, ShapeArtist): diff --git a/src/compas_rhino/artists/cylinderartist.py b/src/compas_rhino/artists/cylinderartist.py index 1c6296180ae2..95a3555214d6 100644 --- a/src/compas_rhino/artists/cylinderartist.py +++ b/src/compas_rhino/artists/cylinderartist.py @@ -5,7 +5,7 @@ from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class CylinderArtist(RhinoArtist, ShapeArtist): diff --git a/src/compas_rhino/artists/frameartist.py b/src/compas_rhino/artists/frameartist.py index c13c4ab26306..a41c55c3db59 100644 --- a/src/compas_rhino/artists/frameartist.py +++ b/src/compas_rhino/artists/frameartist.py @@ -4,7 +4,7 @@ import compas_rhino from compas.artists import PrimitiveArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class FrameArtist(RhinoArtist, PrimitiveArtist): diff --git a/src/compas_rhino/artists/lineartist.py b/src/compas_rhino/artists/lineartist.py index cc7c119ef986..5bdb5604866a 100644 --- a/src/compas_rhino/artists/lineartist.py +++ b/src/compas_rhino/artists/lineartist.py @@ -4,7 +4,7 @@ import compas_rhino from compas.artists import PrimitiveArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class LineArtist(RhinoArtist, PrimitiveArtist): diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index cd7a2a196763..165e96fde2ad 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -13,7 +13,7 @@ from compas.geometry import centroid_points from compas.artists import MeshArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) diff --git a/src/compas_rhino/artists/networkartist.py b/src/compas_rhino/artists/networkartist.py index 56dc61785365..5369efe2ecb7 100644 --- a/src/compas_rhino/artists/networkartist.py +++ b/src/compas_rhino/artists/networkartist.py @@ -9,7 +9,7 @@ from compas.utilities import color_to_colordict from compas.artists import NetworkArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) diff --git a/src/compas_rhino/artists/planeartist.py b/src/compas_rhino/artists/planeartist.py index f520ce26b4de..aa6d56d7fbc5 100644 --- a/src/compas_rhino/artists/planeartist.py +++ b/src/compas_rhino/artists/planeartist.py @@ -3,7 +3,7 @@ from __future__ import division from compas.artists import PrimitiveArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class PlaneArtist(RhinoArtist, PrimitiveArtist): diff --git a/src/compas_rhino/artists/pointartist.py b/src/compas_rhino/artists/pointartist.py index 1049936bbdbe..f78834272c25 100644 --- a/src/compas_rhino/artists/pointartist.py +++ b/src/compas_rhino/artists/pointartist.py @@ -4,7 +4,7 @@ import compas_rhino from compas.artists import PrimitiveArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class PointArtist(RhinoArtist, PrimitiveArtist): diff --git a/src/compas_rhino/artists/polygonartist.py b/src/compas_rhino/artists/polygonartist.py index cd8be320c2b3..87bd28bc700b 100644 --- a/src/compas_rhino/artists/polygonartist.py +++ b/src/compas_rhino/artists/polygonartist.py @@ -4,7 +4,7 @@ import compas_rhino from compas.artists import PrimitiveArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class PolygonArtist(RhinoArtist, PrimitiveArtist): diff --git a/src/compas_rhino/artists/polyhedronartist.py b/src/compas_rhino/artists/polyhedronartist.py index b9a6f05de40c..1e43c5c4cd7a 100644 --- a/src/compas_rhino/artists/polyhedronartist.py +++ b/src/compas_rhino/artists/polyhedronartist.py @@ -4,7 +4,7 @@ import compas_rhino from compas.artists import ShapeArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class PolyhedronArtist(RhinoArtist, ShapeArtist): diff --git a/src/compas_rhino/artists/polylineartist.py b/src/compas_rhino/artists/polylineartist.py index 1fcd9f6375b2..8917da10b0db 100644 --- a/src/compas_rhino/artists/polylineartist.py +++ b/src/compas_rhino/artists/polylineartist.py @@ -4,7 +4,7 @@ import compas_rhino from compas.artists import PrimitiveArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class PolylineArtist(RhinoArtist, PrimitiveArtist): diff --git a/src/compas_rhino/artists/sphereartist.py b/src/compas_rhino/artists/sphereartist.py index 9bfcbc8f9832..c006dc55c9d6 100644 --- a/src/compas_rhino/artists/sphereartist.py +++ b/src/compas_rhino/artists/sphereartist.py @@ -5,7 +5,7 @@ from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class SphereArtist(RhinoArtist, ShapeArtist): diff --git a/src/compas_rhino/artists/torusartist.py b/src/compas_rhino/artists/torusartist.py index 317aa8959469..b1285e3a682a 100644 --- a/src/compas_rhino/artists/torusartist.py +++ b/src/compas_rhino/artists/torusartist.py @@ -5,7 +5,7 @@ from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class TorusArtist(RhinoArtist, ShapeArtist): diff --git a/src/compas_rhino/artists/vectorartist.py b/src/compas_rhino/artists/vectorartist.py index 10debd0337c0..bad6ac879c56 100644 --- a/src/compas_rhino/artists/vectorartist.py +++ b/src/compas_rhino/artists/vectorartist.py @@ -5,7 +5,7 @@ from compas.geometry import Point import compas_rhino from compas.artists import PrimitiveArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist class VectorArtist(RhinoArtist, PrimitiveArtist): diff --git a/src/compas_rhino/artists/volmeshartist.py b/src/compas_rhino/artists/volmeshartist.py index acf102fe2a72..6eaa413ef7d4 100644 --- a/src/compas_rhino/artists/volmeshartist.py +++ b/src/compas_rhino/artists/volmeshartist.py @@ -9,7 +9,7 @@ from compas.geometry import centroid_points from compas.artists import VolMeshArtist -from ._artist import RhinoArtist +from .artist import RhinoArtist colordict = partial(color_to_colordict, colorformat='rgb', normalize=False) From 78f6e96db9e724bd35bbf336260ce7cc9752d02b Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 21 Sep 2021 10:00:53 +0200 Subject: [PATCH 30/71] cooperative multiple inheritance sand rebase blender artists --- src/compas/artists/meshartist.py | 4 +- src/compas/artists/networkartist.py | 4 +- src/compas/artists/primitiveartist.py | 4 +- src/compas/artists/shapeartist.py | 4 +- src/compas/artists/volmeshartist.py | 4 +- src/compas_blender/__init__.py | 6 + src/compas_blender/artists/artist.py | 26 ++- src/compas_blender/artists/frameartist.py | 31 +-- src/compas_blender/artists/meshartist.py | 207 +++--------------- src/compas_blender/artists/networkartist.py | 105 ++------- .../artists/robotmodelartist.py | 27 +-- src/compas_blender/artists/volmeshartist.py | 23 +- src/compas_rhino/artists/artist.py | 4 + src/compas_rhino/artists/boxartist.py | 5 +- src/compas_rhino/artists/capsuleartist.py | 5 +- src/compas_rhino/artists/circleartist.py | 5 +- src/compas_rhino/artists/coneartist.py | 5 +- src/compas_rhino/artists/cylinderartist.py | 5 +- src/compas_rhino/artists/frameartist.py | 5 +- src/compas_rhino/artists/lineartist.py | 5 +- src/compas_rhino/artists/meshartist.py | 5 +- src/compas_rhino/artists/networkartist.py | 5 +- src/compas_rhino/artists/planeartist.py | 5 +- src/compas_rhino/artists/pointartist.py | 5 +- src/compas_rhino/artists/polygonartist.py | 5 +- src/compas_rhino/artists/polyhedronartist.py | 5 +- src/compas_rhino/artists/polylineartist.py | 5 +- src/compas_rhino/artists/robotmodelartist.py | 7 +- src/compas_rhino/artists/sphereartist.py | 5 +- src/compas_rhino/artists/torusartist.py | 5 +- src/compas_rhino/artists/vectorartist.py | 5 +- src/compas_rhino/artists/volmeshartist.py | 5 +- 32 files changed, 164 insertions(+), 382 deletions(-) diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index 1dfa9e6d1105..d4fb5c620b11 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -65,8 +65,8 @@ class MeshArtist(Artist): default_edgecolor = (0, 0, 0) default_facecolor = (0, 0, 0) - def __init__(self, mesh): - super(MeshArtist, self).__init__() + def __init__(self, mesh, **kwargs): + super(MeshArtist, self).__init__(**kwargs) self._mesh = None self._vertices = None self._edges = None diff --git a/src/compas/artists/networkartist.py b/src/compas/artists/networkartist.py index b0aee810d5fd..9170be9b29db 100644 --- a/src/compas/artists/networkartist.py +++ b/src/compas/artists/networkartist.py @@ -56,8 +56,8 @@ class NetworkArtist(Artist): default_nodecolor = (255, 255, 255) default_edgecolor = (0, 0, 0) - def __init__(self, network): - super(NetworkArtist, self).__init__() + def __init__(self, network, **kwargs): + super(NetworkArtist, self).__init__(**kwargs) self._network = None self._nodes = None self._edges = None diff --git a/src/compas/artists/primitiveartist.py b/src/compas/artists/primitiveartist.py index cf8b9fcf4154..a8ff4634ad61 100644 --- a/src/compas/artists/primitiveartist.py +++ b/src/compas/artists/primitiveartist.py @@ -32,8 +32,8 @@ class PrimitiveArtist(Artist): default_color = (0, 0, 0) - def __init__(self, primitive, color=None): - super(PrimitiveArtist, self).__init__() + def __init__(self, primitive, color=None, **kwargs): + super(PrimitiveArtist, self).__init__(**kwargs) self._primitive = None self._color = None self.primitive = primitive diff --git a/src/compas/artists/shapeartist.py b/src/compas/artists/shapeartist.py index 8cb6e5e2b3ec..0b3ed4d9bc4e 100644 --- a/src/compas/artists/shapeartist.py +++ b/src/compas/artists/shapeartist.py @@ -37,8 +37,8 @@ class ShapeArtist(Artist): default_color = (255, 255, 255) - def __init__(self, shape, color=None): - super(ShapeArtist, self).__init__() + def __init__(self, shape, color=None, **kwargs): + super(ShapeArtist, self).__init__(**kwargs) self._u = None self._v = None self._shape = None diff --git a/src/compas/artists/volmeshartist.py b/src/compas/artists/volmeshartist.py index 3e25efbd61db..13eb489d1b32 100644 --- a/src/compas/artists/volmeshartist.py +++ b/src/compas/artists/volmeshartist.py @@ -82,8 +82,8 @@ class VolMeshArtist(Artist): default_facecolor = (210, 210, 210) default_cellcolor = (255, 0, 0) - def __init__(self, volmesh): - super(VolMeshArtist, self).__init__() + def __init__(self, volmesh, **kwargs): + super(VolMeshArtist, self).__init__(**kwargs) self._volmesh = None self._vertices = None self._edges = None diff --git a/src/compas_blender/__init__.py b/src/compas_blender/__init__.py index 63c6aed80408..0f982d01bfb7 100644 --- a/src/compas_blender/__init__.py +++ b/src/compas_blender/__init__.py @@ -23,6 +23,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) @@ -41,6 +42,11 @@ def clear(): bpy.data.collections.remove(block) +def redraw(self): + """Trigger a redraw.""" + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + + __version__ = '1.8.1' diff --git a/src/compas_blender/artists/artist.py b/src/compas_blender/artists/artist.py index fddde69ded7d..13de7e436dbb 100644 --- a/src/compas_blender/artists/artist.py +++ b/src/compas_blender/artists/artist.py @@ -1,3 +1,7 @@ +from typing import Union +from typing import Optional +from typing import Any + import bpy import compas_blender @@ -14,12 +18,26 @@ class BlenderArtist(Artist): """ - def __init__(self): + def __init__(self, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + super().__init__(**kwargs) + self._collection = None + self.collection = collection self.objects = [] - def redraw(self): - """Trigger a redraw.""" - bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + @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): """Delete all objects created by the artist.""" diff --git a/src/compas_blender/artists/frameartist.py b/src/compas_blender/artists/frameartist.py index fdcae61c9bd5..ce354e8a1d47 100644 --- a/src/compas_blender/artists/frameartist.py +++ b/src/compas_blender/artists/frameartist.py @@ -1,14 +1,16 @@ import bpy from typing import List from typing import Optional +from typing import Any from compas.geometry import Frame import compas_blender +from compas.artists import PrimitiveArtist from .artist import BlenderArtist -class FrameArtist(BlenderArtist): +class FrameArtist(BlenderArtist, PrimitiveArtist): """Artist for drawing frames. Parameters @@ -37,34 +39,13 @@ class FrameArtist(BlenderArtist): 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().__init__() - self.collection = collection - self.frame = frame + 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) diff --git a/src/compas_blender/artists/meshartist.py b/src/compas_blender/artists/meshartist.py index 0bb02effca3e..70243a8de494 100644 --- a/src/compas_blender/artists/meshartist.py +++ b/src/compas_blender/artists/meshartist.py @@ -3,6 +3,7 @@ from typing import Optional from typing import Tuple from typing import Union +from typing import Any import bpy @@ -15,13 +16,14 @@ from compas.geometry import scale_vector 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]] -class MeshArtist(BlenderArtist): +class MeshArtist(BlenderArtist, MeshArtist): """A mesh artist defines functionality for visualising COMPAS meshes in Blender. Parameters @@ -34,23 +36,13 @@ class MeshArtist(BlenderArtist): 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() - """ - def __init__(self, mesh: Mesh): - super().__init__() - self._collection = None + def __init__(self, + mesh: Mesh, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + super().__init__(mesh=mesh, collection=collection or mesh.name, **kwargs) self._vertexcollection = None self._edgecollection = None self._facecollection = None @@ -59,184 +51,57 @@ def __init__(self, mesh: Mesh): 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 @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 = {} - - # ========================================================================== - # components + # draw # ========================================================================== def draw(self) -> None: @@ -249,17 +114,15 @@ def draw(self) -> None: """ self.clear() - if self.show_vertices: - self.draw_vertices() - if self.show_faces: - self.draw_faces() - if self.show_edges: - self.draw_edges() + self.draw_vertices() + self.draw_faces() + self.draw_edges() def draw_mesh(self) -> List[bpy.types.Object]: """Draw the mesh.""" vertices, faces = self.mesh.to_vertices_and_faces() obj = compas_blender.draw_mesh(vertices, faces, name=self.mesh.name, collection=self.collection) + self.objects += [obj] return [obj] def draw_vertices(self, @@ -280,18 +143,18 @@ def draw_vertices(self, list of :class:`bpy.types.Object` """ + self.vertex_color = color vertices = vertices or list(self.mesh.vertices()) - vertex_color = colordict(color, vertices, default=self.color_vertices) points = [] for vertex in vertices: points.append({ 'pos': self.mesh.vertex_coordinates(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) + self.objects += objects return objects def draw_faces(self, @@ -312,17 +175,17 @@ def draw_faces(self, list of :class:`bpy.types.Object` """ + self.face_color = color 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] + 'color': self.face_color.get(face, self.default_facecolor) }) objects = compas_blender.draw_faces(facets, self.facecollection) - self.object_face = zip(objects, faces) + self.objects += objects return objects def draw_edges(self, @@ -343,18 +206,18 @@ def draw_edges(self, list of :class:`bpy.types.Object` """ + self.edge_color = color edges = edges or list(self.mesh.edges()) - edge_color = colordict(color, edges, default=self.color_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], + '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) + self.objects += objects return objects # ========================================================================== @@ -397,7 +260,7 @@ def draw_vertexnormals(self, 'name': "{}.vertexnormal.{}".format(self.mesh.name, vertex) }) objects = compas_blender.draw_lines(lines, collection=self.vertexnormalcollection) - self.object_vertexnormal = zip(objects, vertices) + self.objects += objects return objects def draw_facenormals(self, @@ -438,7 +301,7 @@ def draw_facenormals(self, 'color': face_color[face] }) objects = compas_blender.draw_lines(lines, collection=self.facenormalcollection) - self.object_facenormal = zip(objects, faces) + self.objects += objects return objects # ========================================================================== @@ -480,7 +343,7 @@ def draw_vertexlabels(self, 'text': vertex_text[vertex], 'color': vertex_color[vertex]}) objects = compas_blender.draw_texts(labels, collection=self.vertexlabelcollection) - self.object_vertexlabel = zip(objects, vertex_text) + self.objects += objects return objects def draw_edgelabels(self, @@ -517,7 +380,7 @@ def draw_edgelabels(self, '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) + self.objects += objects return objects def draw_facelabels(self, @@ -556,5 +419,5 @@ def draw_facelabels(self, '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) + self.objects += objects return objects diff --git a/src/compas_blender/artists/networkartist.py b/src/compas_blender/artists/networkartist.py index a724019ca7cb..6556f0d6d1c1 100644 --- a/src/compas_blender/artists/networkartist.py +++ b/src/compas_blender/artists/networkartist.py @@ -3,6 +3,7 @@ from typing import Optional from typing import Tuple from typing import Union +from typing import Any import bpy from functools import partial @@ -11,13 +12,14 @@ 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]] -class NetworkArtist(BlenderArtist): +class NetworkArtist(BlenderArtist, NetworkArtist): """Artist for COMPAS network objects. Parameters @@ -36,101 +38,40 @@ class NetworkArtist(BlenderArtist): """ - def __init__(self, network: Network): - super().__init__() + def __init__(self, + network: Network, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + super().__init__(network=network, collection=collection or network.name, **kwargs) 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 @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 - - @object_node.setter - def object_node(self, values): - self._object_node = dict(values) - - @property - def object_edge(self) -> Dict[bpy.types.Object, Tuple[int, int]]: - if not self._object_edge: - self._object_edge = {} - return self._object_edge - - @object_edge.setter - def object_edge(self, values): - self._object_edge = dict(values) - - @property - def object_nodelabel(self) -> Dict[bpy.types.Object, int]: - """Map between Blender object objects and node label identifiers.""" - return self._object_nodelabel - - @object_nodelabel.setter - def object_nodelabel(self, values): - self._object_nodelabel = dict(values) - - @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: """Draw the network. @@ -141,10 +82,8 @@ def draw(self) -> None: """ self.clear() - if self.show_nodes: - self.draw_nodes() - if self.show_edges: - self.draw_edges() + self.draw_nodes() + self.draw_edges() def draw_nodes(self, nodes: Optional[List[int]] = None, @@ -164,17 +103,17 @@ def draw_nodes(self, list of :class:`bpy.types.Object` """ + self.node_color = color 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), 'name': f"{self.network.name}.node.{node}", - 'color': node_color[node], + 'color': self.node_color.get(node, self.default_nodecolor), 'radius': 0.05}) objects = compas_blender.draw_points(points, self.nodecollection) - self.object_node = zip(objects, nodes) + self.objects += objects return objects def draw_edges(self, @@ -195,18 +134,18 @@ def draw_edges(self, list of :class:`bpy.types.Object` """ + self.edge_color = color edges = edges or list(self.network.edges()) - edge_color = colordict(color, edges, default=self.color_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], + '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) + self.objects += objects return objects def draw_nodelabels(self, @@ -245,7 +184,7 @@ def draw_nodelabels(self, 'text': node_text[node], 'color': node_color[node]}) objects = compas_blender.draw_texts(labels, collection=self.nodelabelcollection) - self.object_nodelabel = zip(objects, node_text) + self.objects += objects return objects def draw_edgelabels(self, @@ -283,5 +222,5 @@ def draw_edgelabels(self, '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) + self.objects += objects return objects diff --git a/src/compas_blender/artists/robotmodelartist.py b/src/compas_blender/artists/robotmodelartist.py index f64ab8953767..61f725a9cecd 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 @@ -8,9 +11,10 @@ from compas.geometry import Transformation, Shape from compas.robots import RobotModel from compas.robots.base_artist import BaseRobotModelArtist +from .artist import BlenderArtist -class RobotModelArtist(BaseRobotModelArtist): +class RobotModelArtist(BlenderArtist, BaseRobotModelArtist): """Visualizer for robot models inside a Blender environment. Parameters @@ -21,22 +25,9 @@ class RobotModelArtist(BaseRobotModelArtist): def __init__(self, model: RobotModel, - collection: bpy.types.Collection = None): - self.collection = collection or model.name - super().__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/volmeshartist.py b/src/compas_blender/artists/volmeshartist.py index 5b025c76a236..80001db47a0e 100644 --- a/src/compas_blender/artists/volmeshartist.py +++ b/src/compas_blender/artists/volmeshartist.py @@ -1,16 +1,15 @@ -from .meshartist import MeshArtist +from typing import Optional +from typing import Union +from typing import Any +import bpy +from compas.artists import MeshArtist +from .artist import BlenderArtist -class VolMeshArtist(MeshArtist): - def __init__(self, volmesh): - super().__init__() - self.volmesh = volmesh +class VolMeshArtist(BlenderArtist, MeshArtist): - @property - def volmesh(self): - return self.datastructure - - @volmesh.setter - def volmesh(self, volmesh): - self.datastructure = volmesh + def __init__(self, volmesh, + collection: Optional[Union[str, bpy.types.Collection]] = None, + **kwargs: Any): + super().__init__(volmesh=volmesh, collection=collection or volmesh.name, **kwargs) diff --git a/src/compas_rhino/artists/artist.py b/src/compas_rhino/artists/artist.py index d8635724794a..c1512559d223 100644 --- a/src/compas_rhino/artists/artist.py +++ b/src/compas_rhino/artists/artist.py @@ -10,6 +10,10 @@ 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 dce28f12ab90..13eabe2b1a21 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -18,9 +18,8 @@ class BoxArtist(RhinoArtist, ShapeArtist): The layer that should contain the drawing. """ - def __init__(self, box, layer=None): - super(BoxArtist, self).__init__(box) - self.layer = layer + def __init__(self, box, layer=None, **kwargs): + super(BoxArtist, self).__init__(shape=box, layer=layer, **kwargs) def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the box associated with the artist. diff --git a/src/compas_rhino/artists/capsuleartist.py b/src/compas_rhino/artists/capsuleartist.py index 8d9ca0c95196..e5e2b6789245 100644 --- a/src/compas_rhino/artists/capsuleartist.py +++ b/src/compas_rhino/artists/capsuleartist.py @@ -19,9 +19,8 @@ class CapsuleArtist(RhinoArtist, ShapeArtist): The layer that should contain the drawing. """ - def __init__(self, capsule, layer=None): - super(CapsuleArtist, self).__init__(capsule) - self.layer = layer + def __init__(self, capsule, layer=None, **kwargs): + super(CapsuleArtist, self).__init__(shape=capsule, layer=layer, **kwargs) def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the capsule associated with the artist. diff --git a/src/compas_rhino/artists/circleartist.py b/src/compas_rhino/artists/circleartist.py index 4465c18ebfb6..e35ed0e9b60d 100644 --- a/src/compas_rhino/artists/circleartist.py +++ b/src/compas_rhino/artists/circleartist.py @@ -19,9 +19,8 @@ class CircleArtist(RhinoArtist, PrimitiveArtist): The layer that should contain the drawing. """ - def __init__(self, circle, layer=None): - super(CircleArtist, self).__init__(circle) - self.layer = layer + 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. diff --git a/src/compas_rhino/artists/coneartist.py b/src/compas_rhino/artists/coneartist.py index eb2ea558620d..168424b4c9e7 100644 --- a/src/compas_rhino/artists/coneartist.py +++ b/src/compas_rhino/artists/coneartist.py @@ -20,9 +20,8 @@ class ConeArtist(RhinoArtist, ShapeArtist): """ - def __init__(self, cone, layer=None): - super(ConeArtist, self).__init__(cone) - self.layer = layer + def __init__(self, cone, layer=None, **kwargs): + super(ConeArtist, self).__init__(shape=cone, layer=layer, **kwargs) def draw(self, u=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the cone associated with the artist. diff --git a/src/compas_rhino/artists/cylinderartist.py b/src/compas_rhino/artists/cylinderartist.py index 95a3555214d6..a5d052777013 100644 --- a/src/compas_rhino/artists/cylinderartist.py +++ b/src/compas_rhino/artists/cylinderartist.py @@ -19,9 +19,8 @@ class CylinderArtist(RhinoArtist, ShapeArtist): The layer that should contain the drawing. """ - def __init__(self, cylinder, layer=None): - super(CylinderArtist, self).__init__(cylinder) - self.layer = layer + def __init__(self, cylinder, layer=None, **kwargs): + super(CylinderArtist, self).__init__(shape=cylinder, layer=layer, **kwargs) def draw(self, u=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the cylinder associated with the artist. diff --git a/src/compas_rhino/artists/frameartist.py b/src/compas_rhino/artists/frameartist.py index a41c55c3db59..c3c14ef7ad00 100644 --- a/src/compas_rhino/artists/frameartist.py +++ b/src/compas_rhino/artists/frameartist.py @@ -34,9 +34,8 @@ class FrameArtist(RhinoArtist, PrimitiveArtist): Default is ``(0, 0, 255)``. """ - def __init__(self, frame, layer=None, scale=1.0): - super(FrameArtist, self).__init__(frame) - self.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) diff --git a/src/compas_rhino/artists/lineartist.py b/src/compas_rhino/artists/lineartist.py index 5bdb5604866a..2481d814a94e 100644 --- a/src/compas_rhino/artists/lineartist.py +++ b/src/compas_rhino/artists/lineartist.py @@ -18,9 +18,8 @@ class LineArtist(RhinoArtist, PrimitiveArtist): The layer that should contain the drawing. """ - def __init__(self, line, layer=None): - super(LineArtist, self).__init__(line) - self.layer = layer + 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. diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index 165e96fde2ad..ef1be32784ed 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -29,9 +29,8 @@ class MeshArtist(RhinoArtist, MeshArtist): The name of the layer that will contain the mesh. """ - def __init__(self, mesh, layer=None): - super(MeshArtist, self).__init__(mesh) - self.layer = layer + def __init__(self, mesh, layer=None, **kwargs): + super(MeshArtist, self).__init__(mesh=mesh, layer=layer, **kwargs) def clear_by_name(self): """Clear all objects in the "namespace" of the associated mesh.""" diff --git a/src/compas_rhino/artists/networkartist.py b/src/compas_rhino/artists/networkartist.py index 5369efe2ecb7..e30a44790579 100644 --- a/src/compas_rhino/artists/networkartist.py +++ b/src/compas_rhino/artists/networkartist.py @@ -25,9 +25,8 @@ class NetworkArtist(RhinoArtist, NetworkArtist): The parent layer of the network. """ - def __init__(self, network, layer=None): - super(NetworkArtist, self).__init__(network) - self.layer = layer + def __init__(self, network, layer=None, **kwargs): + super(NetworkArtist, self).__init__(network=network, layer=layer, **kwargs) def clear_by_name(self): """Clear all objects in the "namespace" of the associated network.""" diff --git a/src/compas_rhino/artists/planeartist.py b/src/compas_rhino/artists/planeartist.py index aa6d56d7fbc5..47818ba69384 100644 --- a/src/compas_rhino/artists/planeartist.py +++ b/src/compas_rhino/artists/planeartist.py @@ -17,9 +17,8 @@ class PlaneArtist(RhinoArtist, PrimitiveArtist): The layer that should contain the drawing. """ - def __init__(self, plane, layer=None): - super(PlaneArtist, self).__init__(plane) - self.layer = layer + 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 f78834272c25..483ea9c4afcf 100644 --- a/src/compas_rhino/artists/pointartist.py +++ b/src/compas_rhino/artists/pointartist.py @@ -18,9 +18,8 @@ class PointArtist(RhinoArtist, PrimitiveArtist): The layer that should contain the drawing. """ - def __init__(self, point, layer=None): - super(PointArtist, self).__init__(point) - self.layer = layer + def __init__(self, point, layer=None, **kwargs): + super(PointArtist, self).__init__(primitive=point, layer=layer, **kwargs) def draw(self): """Draw the point. diff --git a/src/compas_rhino/artists/polygonartist.py b/src/compas_rhino/artists/polygonartist.py index 87bd28bc700b..42081bc458c5 100644 --- a/src/compas_rhino/artists/polygonartist.py +++ b/src/compas_rhino/artists/polygonartist.py @@ -18,9 +18,8 @@ class PolygonArtist(RhinoArtist, PrimitiveArtist): The name of the layer that will contain the mesh. """ - def __init__(self, polygon, layer=None): - super(PolygonArtist, self).__init__(polygon) - self.layer = layer + 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. diff --git a/src/compas_rhino/artists/polyhedronartist.py b/src/compas_rhino/artists/polyhedronartist.py index 1e43c5c4cd7a..664970bb552d 100644 --- a/src/compas_rhino/artists/polyhedronartist.py +++ b/src/compas_rhino/artists/polyhedronartist.py @@ -18,9 +18,8 @@ class PolyhedronArtist(RhinoArtist, ShapeArtist): The layer that should contain the drawing. """ - def __init__(self, polyhedron, layer=None): - super(PolyhedronArtist, self).__init__(polyhedron) - self.layer = layer + def __init__(self, polyhedron, layer=None, **kwargs): + super(PolyhedronArtist, self).__init__(shape=polyhedron, layer=layer, **kwargs) def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the polyhedron associated with the artist. diff --git a/src/compas_rhino/artists/polylineartist.py b/src/compas_rhino/artists/polylineartist.py index 8917da10b0db..830f21c1678a 100644 --- a/src/compas_rhino/artists/polylineartist.py +++ b/src/compas_rhino/artists/polylineartist.py @@ -16,9 +16,8 @@ class PolylineArtist(RhinoArtist, PrimitiveArtist): A COMPAS polyline. """ - def __init__(self, polyline, layer=None): - super(PolylineArtist, self).__init__(polyline) - self.layer = layer + 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. diff --git a/src/compas_rhino/artists/robotmodelartist.py b/src/compas_rhino/artists/robotmodelartist.py index bf82717c85df..a96c64f0c86c 100644 --- a/src/compas_rhino/artists/robotmodelartist.py +++ b/src/compas_rhino/artists/robotmodelartist.py @@ -20,7 +20,7 @@ from compas_rhino.geometry.transformations import xform_from_transformation -class RobotModelArtist(BaseRobotModelArtist, RhinoArtist): +class RobotModelArtist(RhinoArtist, BaseRobotModelArtist): """Visualizer for robots inside a Rhino environment. Parameters @@ -31,9 +31,8 @@ class RobotModelArtist(BaseRobotModelArtist, RhinoArtist): 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 c006dc55c9d6..c6da0da04e9c 100644 --- a/src/compas_rhino/artists/sphereartist.py +++ b/src/compas_rhino/artists/sphereartist.py @@ -19,9 +19,8 @@ class SphereArtist(RhinoArtist, ShapeArtist): The layer that should contain the drawing. """ - def __init__(self, sphere, layer=None): - super(SphereArtist, self).__init__(sphere) - self.layer = layer + def __init__(self, sphere, layer=None, **kwargs): + super(SphereArtist, self).__init__(shape=sphere, layer=layer, **kwargs) def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the sphere associated with the artist. diff --git a/src/compas_rhino/artists/torusartist.py b/src/compas_rhino/artists/torusartist.py index b1285e3a682a..cfe427ce007b 100644 --- a/src/compas_rhino/artists/torusartist.py +++ b/src/compas_rhino/artists/torusartist.py @@ -19,9 +19,8 @@ class TorusArtist(RhinoArtist, ShapeArtist): The layer that should contain the drawing. """ - def __init__(self, torus, layer=None): - super(TorusArtist, self).__init__(torus) - self.layer = layer + def __init__(self, torus, layer=None, **kwargs): + super(TorusArtist, self).__init__(shape=torus, layer=layer, **kwargs) def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): """Draw the torus associated with the artist. diff --git a/src/compas_rhino/artists/vectorartist.py b/src/compas_rhino/artists/vectorartist.py index bad6ac879c56..e85c9a33d121 100644 --- a/src/compas_rhino/artists/vectorartist.py +++ b/src/compas_rhino/artists/vectorartist.py @@ -19,9 +19,8 @@ class VectorArtist(RhinoArtist, PrimitiveArtist): The layer that should contain the drawing. """ - def __init__(self, vector, layer=None): - super(VectorArtist, self).__init__(vector) - self.layer = layer + 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. diff --git a/src/compas_rhino/artists/volmeshartist.py b/src/compas_rhino/artists/volmeshartist.py index 6eaa413ef7d4..9509d4919a02 100644 --- a/src/compas_rhino/artists/volmeshartist.py +++ b/src/compas_rhino/artists/volmeshartist.py @@ -25,9 +25,8 @@ class VolMeshArtist(RhinoArtist, VolMeshArtist): The name of the layer that will contain the volmesh. """ - def __init__(self, volmesh, layer=None): - super(VolMeshArtist, self).__init__(volmesh) - self.layer = layer + 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.""" From 339fc488bf0107b43e2f5f8202d790ed36009a80 Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 21 Sep 2021 10:39:09 +0200 Subject: [PATCH 31/71] move robotmodelartist --- src/compas/artists/__init__.py | 2 + src/compas/artists/robotmodelartist.py | 416 +++++++++++++++++ src/compas/robots/base_artist/__init__.py | 7 - src/compas/robots/base_artist/_artist.py | 418 ------------------ .../artists/robotmodelartist.py | 2 +- src/compas_rhino/artists/robotmodelartist.py | 2 +- 6 files changed, 420 insertions(+), 427 deletions(-) delete mode 100644 src/compas/robots/base_artist/__init__.py delete mode 100644 src/compas/robots/base_artist/_artist.py diff --git a/src/compas/artists/__init__.py b/src/compas/artists/__init__.py index 5b1d6ca4b7b6..82a4047edc77 100644 --- a/src/compas/artists/__init__.py +++ b/src/compas/artists/__init__.py @@ -39,6 +39,7 @@ from .meshartist import MeshArtist from .networkartist import NetworkArtist from .primitiveartist import PrimitiveArtist +from .robotmodelartist import RobotModelArtist from .shapeartist import ShapeArtist from .volmeshartist import VolMeshArtist @@ -48,6 +49,7 @@ 'MeshArtist', 'NetworkArtist', 'PrimitiveArtist', + 'RobotModelArtist', 'ShapeArtist', 'VolMeshArtist', ] diff --git a/src/compas/artists/robotmodelartist.py b/src/compas/artists/robotmodelartist.py index e69de29bb2d1..0b2475d9e949 100644 --- a/src/compas/artists/robotmodelartist.py +++ b/src/compas/artists/robotmodelartist.py @@ -0,0 +1,416 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import itertools + +from compas.geometry import Frame +from compas.geometry import Scale +from compas.geometry import Transformation +from compas.robots import Geometry +from compas.robots.model.link import LinkItem + +from .artist import Artist + + +class AbstractRobotModelArtist(object): + + def transform(self, geometry, transformation): + """Transforms a CAD-specific geometry using a **COMPAS** transformation. + + Parameters + ---------- + geometry : object + A CAD-specific (i.e. native) geometry object as returned by :meth:`create_geometry`. + transformation : `Transformation` + **COMPAS** transformation to update the geometry object. + """ + raise NotImplementedError + + def create_geometry(self, geometry, name=None, color=None): + """Draw a **COMPAS** geometry in the respective CAD environment. + + Note + ---- + This is an abstract method that needs to be implemented by derived classes. + + Parameters + ---------- + geometry : :class:`Mesh` + Instance of a **COMPAS** mesh + name : str, optional + The name of the mesh to draw. + + Returns + ------- + object + CAD-specific geometry + """ + raise NotImplementedError + + +class BaseRobotModelArtist(AbstractRobotModelArtist, Artist): + """Provides common functionality to most robot model artist implementations. + + In **COMPAS**, the `artists` are classes that assist with the visualization of + datastructures and models, in a way that maintains the data separated from the + specific CAD interfaces, while providing a way to leverage native performance + of the CAD environment. + + There are two methods that implementers of this base class should provide, one + is concerned with the actual creation of geometry in the native format of the + CAD environment (:meth:`create_geometry`) and the other is one to apply a transformation + to geometry (:meth:`transform`). + + Attributes + ---------- + model : :class:`compas.robots.RobotModel` + Instance of a robot model. + """ + + def __init__(self, model): + super(BaseRobotModelArtist, self).__init__() + self.model = model + self.create() + self.scale_factor = 1. + self.attached_tool_model = None + self.attached_items = {} + + def attach_tool_model(self, tool_model): + """Attach a tool to the robot artist for visualization. + + Parameters + ---------- + tool_model : :class:`compas.robots.ToolModel` + The tool that should be attached to the robot's flange. + """ + self.attached_tool_model = tool_model + self.create(tool_model.root, 'attached_tool') + + if not tool_model.link_name: + link = self.model.get_end_effector_link() + tool_model.link_name = link.name + else: + link = self.model.get_link_by_name(tool_model.link_name) + + ee_frame = link.parent_joint.origin.copy() + initial_transformation = Transformation.from_frame_to_frame(Frame.worldXY(), ee_frame) + + sample_geometry = link.collision[0] if link.collision else link.visual[0] if link.visual else None + + if hasattr(sample_geometry, 'current_transformation'): + relative_transformation = sample_geometry.current_transformation + else: + relative_transformation = Transformation() + + transformation = relative_transformation.concatenated(initial_transformation) + + self.update_tool(transformation=transformation) + + tool_model.parent_joint_name = link.parent_joint.name + + def detach_tool_model(self): + """Detach the tool. + """ + self.attached_tool_model = None + + def attach_mesh(self, mesh, name, link=None, frame=None): + """Rigidly attaches a compas mesh to a given link for visualization. + + Parameters + ---------- + mesh : :class:`compas.datastructures.Mesh` + The mesh to attach to the robot model. + name : :obj:`str` + The identifier of the mesh. + link : :class:`compas.robots.Link` + The link within the robot model or tool model to attach the mesh to. Optional. + Defaults to the model's end effector link. + frame : :class:`compas.geometry.Frame` + The frame of the mesh. Defaults to :meth:`compas.geometry.Frame.worldXY`. + + Returns + ------- + ``None`` + """ + if not link: + link = self.model.get_end_effector_link() + transformation = Transformation.from_frame(frame) if frame else Transformation() + + sample_geometry = None + + while sample_geometry is None: + sample_geometry = link.collision[0] if link.collision else link.visual[0] if link.visual else None + link = self.model.get_link_by_name(link.parent_joint.parent.link) + + native_mesh = self.create_geometry(mesh) + init_transformation = transformation * sample_geometry.init_transformation + self.transform(native_mesh, sample_geometry.current_transformation * init_transformation) + + item = LinkItem() + item.native_geometry = [native_mesh] + item.init_transformation = init_transformation + item.current_transformation = sample_geometry.current_transformation + + self.attached_items.setdefault(link.name, {})[name] = item + + def detach_mesh(self, name): + """Removes attached collision meshes with a given name. + + Parameters + ---------- + name : :obj:`str` + The identifier of the mesh. + + Returns + ------- + ``None`` + """ + for _, items in self.attached_items: + items.pop(name, None) + + def create(self, link=None, context=None): + """Recursive function that triggers the drawing of the robot model's geometry. + + This method delegates the geometry drawing to the :meth:`create_geometry` + method. It transforms the geometry based on the saved initial + transformation from the robot model. + + Parameters + ---------- + link : :class:`compas.robots.Link`, optional + Link instance to create. Defaults to the robot model's root. + context : :obj:`str`, optional + Subdomain identifier to insert in the mesh names. + + Returns + ------- + None + """ + if link is None: + link = self.model.root + + for item in itertools.chain(link.visual, link.collision): + meshes = Geometry._get_item_meshes(item) + + if meshes: + is_visual = hasattr(item, 'get_color') + color = item.get_color() if is_visual else None + + native_geometry = [] + for i, mesh in enumerate(meshes): + mesh_type = 'visual' if is_visual else 'collision' + if not context: + mesh_name_components = [self.model.name, mesh_type, link.name, str(i)] + else: + mesh_name_components = [self.model.name, mesh_type, context, link.name, str(i)] + mesh_name = '.'.join(mesh_name_components) + native_mesh = self.create_geometry(mesh, name=mesh_name, color=color) + + self.transform(native_mesh, item.init_transformation) + + native_geometry.append(native_mesh) + + item.native_geometry = native_geometry + item.current_transformation = Transformation() + + for child_joint in link.joints: + self.create(child_joint.child_link) + + def meshes(self, link=None, visual=True, collision=False, attached_meshes=True): + """Returns all compas meshes of the model. + + Parameters + ---------- + link : :class:`compas.robots.Link`, optional + Base link instance. Defaults to the robot model's root. + visual : :obj:`bool`, optional + Whether to include the robot's visual meshes. Defaults + to ``True``. + collision : :obj:`bool`, optional + Whether to include the robot's collision meshes. Defaults + to ``False``. + attached_meshes : :obj:`bool`, optional + Whether to include the robot's attached meshes. Defaults + to ``True``. + + Returns + ------- + :obj:`list` of :class:`compas.datastructures.Mesh` + """ + if link is None: + link = self.model.root + + meshes = [] + items = [] + if visual: + items += link.visual + if collision: + items += link.collision + if attached_meshes: + items += list(self.attached_items.get(link.name, {}).values()) + for item in items: + new_meshes = Geometry._get_item_meshes(item) + for mesh in new_meshes: + mesh.transform(item.current_transformation) + meshes += new_meshes + for child_joint in link.joints: + meshes += self.meshes(child_joint.child_link, visual, collision, attached_meshes) + return meshes + + def scale(self, factor): + """Scales the robot model's geometry by factor (absolute). + + Parameters + ---------- + factor : float + The factor to scale the robot with. + + Returns + ------- + None + """ + self.model.scale(factor) + + relative_factor = factor / self.scale_factor + transformation = Scale.from_factors([relative_factor] * 3) + self.scale_link(self.model.root, transformation) + self.scale_factor = factor + + def scale_link(self, link, transformation): + """Recursive function to apply the scale transformation on each link. + """ + self._scale_link_helper(link, transformation) + + if self.attached_tool_model: + self._scale_link_helper(self.attached_tool_model.root, transformation) + + def _scale_link_helper(self, link, transformation): + for item in itertools.chain(link.visual, link.collision): + # Some links have only collision geometry, not visual. These meshes + # have not been loaded. + if item.native_geometry: + for geometry in item.native_geometry: + self.transform(geometry, transformation) + + for child_joint in link.joints: + self._scale_link_helper(child_joint.child_link, transformation) + + def _apply_transformation_on_transformed_link(self, item, transformation): + """Applies a transformation on a link that is already transformed. + + Calculates the relative transformation and applies it to the link + geometry. This is to prevent the recreation of large meshes. + + Parameters + ---------- + item: :class:`compas.robots.Visual` or :class:`compas.robots.Collision` + The visual or collidable object of a link. + transformation: :class:`Transformation` + The (absolute) transformation to apply onto the link's geometry. + + Returns + ------- + None + """ + if getattr(item, 'current_transformation'): + relative_transformation = transformation * item.current_transformation.inverse() + else: + relative_transformation = transformation + for native_geometry in item.native_geometry or []: + self.transform(native_geometry, relative_transformation) + item.current_transformation = transformation + + def update(self, joint_state, visual=True, collision=True): + """Triggers the update of the robot geometry. + + Parameters + ---------- + joint_state : :obj:`dict` or :class:`compas.robots.Configuration` + A dictionary with joint names as keys and joint positions as values. + visual : bool, optional + ``True`` if the visual geometry should be also updated, otherwise ``False``. + Defaults to ``True``. + collision : bool, optional + ``True`` if the collision geometry should be also updated, otherwise ``False``. + Defaults to ``True``. + """ + _ = self._update(self.model, joint_state, visual, collision) + if self.attached_tool_model: + frame = self.model.forward_kinematics(joint_state, link_name=self.attached_tool_model.link_name) + self.update_tool(visual=visual, collision=collision, transformation=Transformation.from_frame_to_frame(Frame.worldXY(), frame)) + + def _update(self, model, joint_state, visual=True, collision=True, parent_transformation=None): + transformations = model.compute_transformations(joint_state, parent_transformation=parent_transformation) + for j in model.iter_joints(): + self._transform_link_geometry(j.child_link, transformations[j.name], collision) + return transformations + + def _transform_link_geometry(self, link, transformation, collision=True): + for item in link.visual: + self._apply_transformation_on_transformed_link(item, transformation) + if collision: + for item in link.collision: + # some links have only collision geometry, not visual. These meshes have not been loaded. + if item.native_geometry: + self._apply_transformation_on_transformed_link(item, transformation) + for item in self.attached_items.get(link.name, {}).values(): + self._apply_transformation_on_transformed_link(item, transformation) + + def update_tool(self, joint_state=None, visual=True, collision=True, transformation=None): + """Triggers the update of the robot geometry of the tool. + + Parameters + ---------- + joint_state : :obj:`dict`or :class:`compas.robots.Configuration`, optional + A dictionary with joint names as keys and joint positions as values. + Defaults to an empty dictionary. + transformation : :class:`compas.geometry.Transformation`, optional + The (absolute) transformation to apply to the entire tool's geometry. + If ``None`` is given, no additional transformation will be applied. + Defaults to ``None``. + visual : bool, optional + ``True`` if the visual geometry should be also updated, otherwise ``False``. + Defaults to ``True``. + collision : bool, optional + ``True`` if the collision geometry should be also updated, otherwise ``False``. + Defaults to ``True``. + """ + joint_state = joint_state or {} + if self.attached_tool_model: + if transformation is None: + transformation = self.attached_tool_model.current_transformation + self._transform_link_geometry(self.attached_tool_model.root, transformation, collision) + self._update(self.attached_tool_model, joint_state, visual, collision, transformation) + self.attached_tool_model.current_transformation = transformation + + def draw_visual(self): + """Draws all visual geometry of the robot model.""" + for native_geometry in self._iter_geometry(self.model, 'visual'): + yield native_geometry + if self.attached_tool_model: + for native_geometry in self._iter_geometry(self.attached_tool_model, 'visual'): + yield native_geometry + + def draw_collision(self): + """Draws all collision geometry of the robot model.""" + for native_geometry in self._iter_geometry(self.model, 'collision'): + yield native_geometry + if self.attached_tool_model: + for native_geometry in self._iter_geometry(self.attached_tool_model, 'collision'): + yield native_geometry + + def draw_attached_meshes(self): + """Draws all meshes attached to the robot model.""" + for items in self.attached_items.values(): + for item in items.values(): + for native_mesh in item.native_geometry: + yield native_mesh + + @staticmethod + def _iter_geometry(model, geometry_type): + for link in model.iter_links(): + for item in getattr(link, geometry_type): + if item.native_geometry: + for native_geometry in item.native_geometry: + yield native_geometry diff --git a/src/compas/robots/base_artist/__init__.py b/src/compas/robots/base_artist/__init__.py deleted file mode 100644 index a1d5b96942d3..000000000000 --- a/src/compas/robots/base_artist/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -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('_')] diff --git a/src/compas/robots/base_artist/_artist.py b/src/compas/robots/base_artist/_artist.py deleted file mode 100644 index e0174e8fcfda..000000000000 --- a/src/compas/robots/base_artist/_artist.py +++ /dev/null @@ -1,418 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import itertools - -from compas.geometry import Frame -from compas.geometry import Scale -from compas.geometry import Transformation -from compas.robots import Geometry -from compas.robots.model.link import LinkItem - - -__all__ = [ - 'BaseRobotModelArtist' -] - - -class AbstractRobotModelArtist(object): - def transform(self, geometry, transformation): - """Transforms a CAD-specific geometry using a **COMPAS** transformation. - - Parameters - ---------- - geometry : object - A CAD-specific (i.e. native) geometry object as returned by :meth:`create_geometry`. - transformation : `Transformation` - **COMPAS** transformation to update the geometry object. - """ - raise NotImplementedError - - def create_geometry(self, geometry, name=None, color=None): - """Draw a **COMPAS** geometry in the respective CAD environment. - - Note - ---- - This is an abstract method that needs to be implemented by derived classes. - - Parameters - ---------- - geometry : :class:`Mesh` - Instance of a **COMPAS** mesh - name : str, optional - The name of the mesh to draw. - - Returns - ------- - object - CAD-specific geometry - """ - raise NotImplementedError - - -class BaseRobotModelArtist(AbstractRobotModelArtist): - """Provides common functionality to most robot model artist implementations. - - In **COMPAS**, the `artists` are classes that assist with the visualization of - datastructures and models, in a way that maintains the data separated from the - specific CAD interfaces, while providing a way to leverage native performance - of the CAD environment. - - There are two methods that implementers of this base class should provide, one - is concerned with the actual creation of geometry in the native format of the - CAD environment (:meth:`create_geometry`) and the other is one to apply a transformation - to geometry (:meth:`transform`). - - Attributes - ---------- - model : :class:`compas.robots.RobotModel` - Instance of a robot model. - """ - - def __init__(self, model): - super(BaseRobotModelArtist, self).__init__() - self.model = model - self.create() - self.scale_factor = 1. - self.attached_tool_model = None - self.attached_items = {} - - def attach_tool_model(self, tool_model): - """Attach a tool to the robot artist for visualization. - - Parameters - ---------- - tool_model : :class:`compas.robots.ToolModel` - The tool that should be attached to the robot's flange. - """ - self.attached_tool_model = tool_model - self.create(tool_model.root, 'attached_tool') - - if not tool_model.link_name: - link = self.model.get_end_effector_link() - tool_model.link_name = link.name - else: - link = self.model.get_link_by_name(tool_model.link_name) - - ee_frame = link.parent_joint.origin.copy() - initial_transformation = Transformation.from_frame_to_frame(Frame.worldXY(), ee_frame) - - sample_geometry = link.collision[0] if link.collision else link.visual[0] if link.visual else None - - if hasattr(sample_geometry, 'current_transformation'): - relative_transformation = sample_geometry.current_transformation - else: - relative_transformation = Transformation() - - transformation = relative_transformation.concatenated(initial_transformation) - - self.update_tool(transformation=transformation) - - tool_model.parent_joint_name = link.parent_joint.name - - def detach_tool_model(self): - """Detach the tool. - """ - self.attached_tool_model = None - - def attach_mesh(self, mesh, name, link=None, frame=None): - """Rigidly attaches a compas mesh to a given link for visualization. - - Parameters - ---------- - mesh : :class:`compas.datastructures.Mesh` - The mesh to attach to the robot model. - name : :obj:`str` - The identifier of the mesh. - link : :class:`compas.robots.Link` - The link within the robot model or tool model to attach the mesh to. Optional. - Defaults to the model's end effector link. - frame : :class:`compas.geometry.Frame` - The frame of the mesh. Defaults to :meth:`compas.geometry.Frame.worldXY`. - - Returns - ------- - ``None`` - """ - if not link: - link = self.model.get_end_effector_link() - transformation = Transformation.from_frame(frame) if frame else Transformation() - - sample_geometry = None - - while sample_geometry is None: - sample_geometry = link.collision[0] if link.collision else link.visual[0] if link.visual else None - link = self.model.get_link_by_name(link.parent_joint.parent.link) - - native_mesh = self.create_geometry(mesh) - init_transformation = transformation * sample_geometry.init_transformation - self.transform(native_mesh, sample_geometry.current_transformation * init_transformation) - - item = LinkItem() - item.native_geometry = [native_mesh] - item.init_transformation = init_transformation - item.current_transformation = sample_geometry.current_transformation - - self.attached_items.setdefault(link.name, {})[name] = item - - def detach_mesh(self, name): - """Removes attached collision meshes with a given name. - - Parameters - ---------- - name : :obj:`str` - The identifier of the mesh. - - Returns - ------- - ``None`` - """ - for _, items in self.attached_items: - items.pop(name, None) - - def create(self, link=None, context=None): - """Recursive function that triggers the drawing of the robot model's geometry. - - This method delegates the geometry drawing to the :meth:`create_geometry` - method. It transforms the geometry based on the saved initial - transformation from the robot model. - - Parameters - ---------- - link : :class:`compas.robots.Link`, optional - Link instance to create. Defaults to the robot model's root. - context : :obj:`str`, optional - Subdomain identifier to insert in the mesh names. - - Returns - ------- - None - """ - if link is None: - link = self.model.root - - for item in itertools.chain(link.visual, link.collision): - meshes = Geometry._get_item_meshes(item) - - if meshes: - is_visual = hasattr(item, 'get_color') - color = item.get_color() if is_visual else None - - native_geometry = [] - for i, mesh in enumerate(meshes): - mesh_type = 'visual' if is_visual else 'collision' - if not context: - mesh_name_components = [self.model.name, mesh_type, link.name, str(i)] - else: - mesh_name_components = [self.model.name, mesh_type, context, link.name, str(i)] - mesh_name = '.'.join(mesh_name_components) - native_mesh = self.create_geometry(mesh, name=mesh_name, color=color) - - self.transform(native_mesh, item.init_transformation) - - native_geometry.append(native_mesh) - - item.native_geometry = native_geometry - item.current_transformation = Transformation() - - for child_joint in link.joints: - self.create(child_joint.child_link) - - def meshes(self, link=None, visual=True, collision=False, attached_meshes=True): - """Returns all compas meshes of the model. - - Parameters - ---------- - link : :class:`compas.robots.Link`, optional - Base link instance. Defaults to the robot model's root. - visual : :obj:`bool`, optional - Whether to include the robot's visual meshes. Defaults - to ``True``. - collision : :obj:`bool`, optional - Whether to include the robot's collision meshes. Defaults - to ``False``. - attached_meshes : :obj:`bool`, optional - Whether to include the robot's attached meshes. Defaults - to ``True``. - - Returns - ------- - :obj:`list` of :class:`compas.datastructures.Mesh` - """ - if link is None: - link = self.model.root - - meshes = [] - items = [] - if visual: - items += link.visual - if collision: - items += link.collision - if attached_meshes: - items += list(self.attached_items.get(link.name, {}).values()) - for item in items: - new_meshes = Geometry._get_item_meshes(item) - for mesh in new_meshes: - mesh.transform(item.current_transformation) - meshes += new_meshes - for child_joint in link.joints: - meshes += self.meshes(child_joint.child_link, visual, collision, attached_meshes) - return meshes - - def scale(self, factor): - """Scales the robot model's geometry by factor (absolute). - - Parameters - ---------- - factor : float - The factor to scale the robot with. - - Returns - ------- - None - """ - self.model.scale(factor) - - relative_factor = factor / self.scale_factor - transformation = Scale.from_factors([relative_factor] * 3) - self.scale_link(self.model.root, transformation) - self.scale_factor = factor - - def scale_link(self, link, transformation): - """Recursive function to apply the scale transformation on each link. - """ - self._scale_link_helper(link, transformation) - - if self.attached_tool_model: - self._scale_link_helper(self.attached_tool_model.root, transformation) - - def _scale_link_helper(self, link, transformation): - for item in itertools.chain(link.visual, link.collision): - # Some links have only collision geometry, not visual. These meshes - # have not been loaded. - if item.native_geometry: - for geometry in item.native_geometry: - self.transform(geometry, transformation) - - for child_joint in link.joints: - self._scale_link_helper(child_joint.child_link, transformation) - - def _apply_transformation_on_transformed_link(self, item, transformation): - """Applies a transformation on a link that is already transformed. - - Calculates the relative transformation and applies it to the link - geometry. This is to prevent the recreation of large meshes. - - Parameters - ---------- - item: :class:`compas.robots.Visual` or :class:`compas.robots.Collision` - The visual or collidable object of a link. - transformation: :class:`Transformation` - The (absolute) transformation to apply onto the link's geometry. - - Returns - ------- - None - """ - if getattr(item, 'current_transformation'): - relative_transformation = transformation * item.current_transformation.inverse() - else: - relative_transformation = transformation - for native_geometry in item.native_geometry or []: - self.transform(native_geometry, relative_transformation) - item.current_transformation = transformation - - def update(self, joint_state, visual=True, collision=True): - """Triggers the update of the robot geometry. - - Parameters - ---------- - joint_state : :obj:`dict` or :class:`compas.robots.Configuration` - A dictionary with joint names as keys and joint positions as values. - visual : bool, optional - ``True`` if the visual geometry should be also updated, otherwise ``False``. - Defaults to ``True``. - collision : bool, optional - ``True`` if the collision geometry should be also updated, otherwise ``False``. - Defaults to ``True``. - """ - _ = self._update(self.model, joint_state, visual, collision) - if self.attached_tool_model: - frame = self.model.forward_kinematics(joint_state, link_name=self.attached_tool_model.link_name) - self.update_tool(visual=visual, collision=collision, transformation=Transformation.from_frame_to_frame(Frame.worldXY(), frame)) - - def _update(self, model, joint_state, visual=True, collision=True, parent_transformation=None): - transformations = model.compute_transformations(joint_state, parent_transformation=parent_transformation) - for j in model.iter_joints(): - self._transform_link_geometry(j.child_link, transformations[j.name], collision) - return transformations - - def _transform_link_geometry(self, link, transformation, collision=True): - for item in link.visual: - self._apply_transformation_on_transformed_link(item, transformation) - if collision: - for item in link.collision: - # some links have only collision geometry, not visual. These meshes have not been loaded. - if item.native_geometry: - self._apply_transformation_on_transformed_link(item, transformation) - for item in self.attached_items.get(link.name, {}).values(): - self._apply_transformation_on_transformed_link(item, transformation) - - def update_tool(self, joint_state=None, visual=True, collision=True, transformation=None): - """Triggers the update of the robot geometry of the tool. - - Parameters - ---------- - joint_state : :obj:`dict`or :class:`compas.robots.Configuration`, optional - A dictionary with joint names as keys and joint positions as values. - Defaults to an empty dictionary. - transformation : :class:`compas.geometry.Transformation`, optional - The (absolute) transformation to apply to the entire tool's geometry. - If ``None`` is given, no additional transformation will be applied. - Defaults to ``None``. - visual : bool, optional - ``True`` if the visual geometry should be also updated, otherwise ``False``. - Defaults to ``True``. - collision : bool, optional - ``True`` if the collision geometry should be also updated, otherwise ``False``. - Defaults to ``True``. - """ - joint_state = joint_state or {} - if self.attached_tool_model: - if transformation is None: - transformation = self.attached_tool_model.current_transformation - self._transform_link_geometry(self.attached_tool_model.root, transformation, collision) - self._update(self.attached_tool_model, joint_state, visual, collision, transformation) - self.attached_tool_model.current_transformation = transformation - - def draw_visual(self): - """Draws all visual geometry of the robot model.""" - for native_geometry in self._iter_geometry(self.model, 'visual'): - yield native_geometry - if self.attached_tool_model: - for native_geometry in self._iter_geometry(self.attached_tool_model, 'visual'): - yield native_geometry - - def draw_collision(self): - """Draws all collision geometry of the robot model.""" - for native_geometry in self._iter_geometry(self.model, 'collision'): - yield native_geometry - if self.attached_tool_model: - for native_geometry in self._iter_geometry(self.attached_tool_model, 'collision'): - yield native_geometry - - def draw_attached_meshes(self): - """Draws all meshes attached to the robot model.""" - for items in self.attached_items.values(): - for item in items.values(): - for native_mesh in item.native_geometry: - yield native_mesh - - @staticmethod - def _iter_geometry(model, geometry_type): - for link in model.iter_links(): - for item in getattr(link, geometry_type): - if item.native_geometry: - for native_geometry in item.native_geometry: - yield native_geometry diff --git a/src/compas_blender/artists/robotmodelartist.py b/src/compas_blender/artists/robotmodelartist.py index 61f725a9cecd..e3874c7a91b4 100644 --- a/src/compas_blender/artists/robotmodelartist.py +++ b/src/compas_blender/artists/robotmodelartist.py @@ -10,7 +10,7 @@ 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 BaseRobotModelArtist from .artist import BlenderArtist diff --git a/src/compas_rhino/artists/robotmodelartist.py b/src/compas_rhino/artists/robotmodelartist.py index a96c64f0c86c..f914727d1ce5 100644 --- a/src/compas_rhino/artists/robotmodelartist.py +++ b/src/compas_rhino/artists/robotmodelartist.py @@ -13,7 +13,7 @@ from compas.geometry import centroid_polygon from compas.utilities import pairwise -from compas.robots.base_artist import BaseRobotModelArtist +from compas.artists import BaseRobotModelArtist import compas_rhino from compas_rhino.artists import RhinoArtist From 5c4972383a3b10f6d5f99a3f7e374e14f00c7ba3 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 10:14:01 +0200 Subject: [PATCH 32/71] default facecolor white --- src/compas/artists/meshartist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index d4fb5c620b11..d2306f1397ae 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -63,7 +63,7 @@ class MeshArtist(Artist): default_color = (0, 0, 0) default_vertexcolor = (255, 255, 255) default_edgecolor = (0, 0, 0) - default_facecolor = (0, 0, 0) + default_facecolor = (255, 255, 255) def __init__(self, mesh, **kwargs): super(MeshArtist, self).__init__(**kwargs) From e528daafcbcd9e27a85f20a6645fc63dad1fa6c3 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 10:14:11 +0200 Subject: [PATCH 33/71] import base artist --- src/compas/artists/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/compas/artists/__init__.py b/src/compas/artists/__init__.py index 82a4047edc77..eb8d20fa0431 100644 --- a/src/compas/artists/__init__.py +++ b/src/compas/artists/__init__.py @@ -13,6 +13,7 @@ :nosignatures: Artist + BaseRobotModelArtist MeshArtist NetworkArtist PrimitiveArtist @@ -39,7 +40,7 @@ from .meshartist import MeshArtist from .networkartist import NetworkArtist from .primitiveartist import PrimitiveArtist -from .robotmodelartist import RobotModelArtist +from .robotmodelartist import BaseRobotModelArtist from .shapeartist import ShapeArtist from .volmeshartist import VolMeshArtist @@ -49,7 +50,7 @@ 'MeshArtist', 'NetworkArtist', 'PrimitiveArtist', - 'RobotModelArtist', + 'BaseRobotModelArtist', 'ShapeArtist', 'VolMeshArtist', ] From d67c8061a8850aa62a6730197e82996b1e253089 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 11:00:48 +0200 Subject: [PATCH 34/71] add box artist for blender --- src/compas_blender/artists/__init__.py | 3 ++ src/compas_blender/artists/boxartist.py | 42 +++++++++++++++++++++++++ src/compas_blender/utilities/drawing.py | 21 ------------- 3 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 src/compas_blender/artists/boxartist.py diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 78494dbdc441..90731b4bf5b5 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -35,12 +35,14 @@ from compas.artists import Artist from compas.artists import DataArtistNotRegistered +from compas.geometry import Box from compas.geometry import Frame from compas.datastructures import Mesh from compas.datastructures import Network from compas.robots import RobotModel from .artist import BlenderArtist # noqa: F401 +from .boxartist import BoxArtist from .frameartist import FrameArtist from .meshartist import MeshArtist from .networkartist import NetworkArtist @@ -50,6 +52,7 @@ ) +Artist.register(Box, BoxArtist) Artist.register(Frame, FrameArtist) Artist.register(Mesh, MeshArtist) Artist.register(Network, NetworkArtist) diff --git a/src/compas_blender/artists/boxartist.py b/src/compas_blender/artists/boxartist.py new file mode 100644 index 000000000000..a1f4f7e308f1 --- /dev/null +++ b/src/compas_blender/artists/boxartist.py @@ -0,0 +1,42 @@ +from typing import Optional +from typing import Any + +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. + layer : str, optional + The layer that should contain the drawing. + """ + + def __init__(self, + box: Box, + collection: Optional[bpy.types.Collection] = None, + **kwargs: Any): + super().__init__(shape=box, collection=collection or box.name, **kwargs) + + def draw(self): + """Draw the box associated with the artist. + + Returns + ------- + list + The objects created in Blender. + """ + vertices = self.shape.vertices + faces = self.shape.faces + objects = [] + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) + objects.append(obj) + self.objects += objects + return objects 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.""" From 5ee4af5fbd2b36fadbb0ccc66d13c3e324c4e6a6 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 11:30:38 +0200 Subject: [PATCH 35/71] clean up and add sphere artist to blender --- src/compas_blender/artists/__init__.py | 12 +++-- src/compas_blender/artists/boxartist.py | 7 +-- src/compas_blender/artists/frameartist.py | 10 ++-- src/compas_blender/artists/meshartist.py | 16 +++++- src/compas_blender/artists/networkartist.py | 16 +++--- .../artists/robotmodelartist.py | 2 + src/compas_blender/artists/sphereartist.py | 54 +++++++++++++++++++ src/compas_blender/artists/volmeshartist.py | 1 + 8 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 src/compas_blender/artists/sphereartist.py diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 90731b4bf5b5..eb8b80ad8034 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -37,6 +37,7 @@ from compas.geometry import Box from compas.geometry import Frame +from compas.geometry import Sphere from compas.datastructures import Mesh from compas.datastructures import Network from compas.robots import RobotModel @@ -46,10 +47,8 @@ from .frameartist import FrameArtist from .meshartist import MeshArtist from .networkartist import NetworkArtist -from .robotmodelartist import ( # noqa: F401 - BaseRobotModelArtist, - RobotModelArtist -) +from .robotmodelartist import RobotModelArtist +from .sphereartist import SphereArtist Artist.register(Box, BoxArtist) @@ -57,6 +56,7 @@ Artist.register(Mesh, MeshArtist) Artist.register(Network, NetworkArtist) Artist.register(RobotModel, RobotModelArtist) +Artist.register(Sphere, SphereArtist) @plugin(category='factories', pluggable_name='new_artist', requires=['bpy']) @@ -74,8 +74,10 @@ def new_artist_blender(cls, *args, **kwargs): __all__ = [ + 'BoxArtist', 'FrameArtist', 'NetworkArtist', 'MeshArtist', - 'RobotModelArtist' + 'RobotModelArtist', + 'SphereArtist', ] diff --git a/src/compas_blender/artists/boxartist.py b/src/compas_blender/artists/boxartist.py index a1f4f7e308f1..2cc8840cafa1 100644 --- a/src/compas_blender/artists/boxartist.py +++ b/src/compas_blender/artists/boxartist.py @@ -1,5 +1,6 @@ from typing import Optional from typing import Any +from typing import Union import bpy import compas_blender @@ -15,13 +16,13 @@ class BoxArtist(BlenderArtist, ShapeArtist): ---------- box : :class:`compas.geometry.Box` A COMPAS box. - layer : str, optional - The layer that should contain the drawing. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. """ def __init__(self, box: Box, - collection: Optional[bpy.types.Collection] = None, + collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): super().__init__(shape=box, collection=collection or box.name, **kwargs) diff --git a/src/compas_blender/artists/frameartist.py b/src/compas_blender/artists/frameartist.py index ce354e8a1d47..0c726fc007c0 100644 --- a/src/compas_blender/artists/frameartist.py +++ b/src/compas_blender/artists/frameartist.py @@ -1,7 +1,9 @@ -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 @@ -17,8 +19,8 @@ class FrameArtist(BlenderArtist, PrimitiveArtist): ---------- 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. @@ -42,7 +44,7 @@ class FrameArtist(BlenderArtist, PrimitiveArtist): """ def __init__(self, frame: Frame, - collection: Optional[bpy.types.Collection] = None, + collection: Optional[Union[str, bpy.types.Collection]] = None, scale: float = 1.0, **kwargs: Any): super().__init__(primitive=frame, collection=collection or frame.name, **kwargs) diff --git a/src/compas_blender/artists/meshartist.py b/src/compas_blender/artists/meshartist.py index 70243a8de494..194695633db3 100644 --- a/src/compas_blender/artists/meshartist.py +++ b/src/compas_blender/artists/meshartist.py @@ -30,11 +30,23 @@ class MeshArtist(BlenderArtist, MeshArtist): ---------- mesh : :class:`compas.datastructures.Mesh` A COMPAS mesh. + collection: str or :class:`bpy.types.Collection` + The name of the collection the object belongs to. Attributes ---------- - mesh : :class:`compas.datastructures.Mesh` - The COMPAS mesh associated with the artist. + 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. """ diff --git a/src/compas_blender/artists/networkartist.py b/src/compas_blender/artists/networkartist.py index 6556f0d6d1c1..7d2f4f692b52 100644 --- a/src/compas_blender/artists/networkartist.py +++ b/src/compas_blender/artists/networkartist.py @@ -26,15 +26,19 @@ class NetworkArtist(BlenderArtist, NetworkArtist): ---------- 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. 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. """ diff --git a/src/compas_blender/artists/robotmodelartist.py b/src/compas_blender/artists/robotmodelartist.py index e3874c7a91b4..5e113cd01955 100644 --- a/src/compas_blender/artists/robotmodelartist.py +++ b/src/compas_blender/artists/robotmodelartist.py @@ -21,6 +21,8 @@ class RobotModelArtist(BlenderArtist, BaseRobotModelArtist): ---------- 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, diff --git a/src/compas_blender/artists/sphereartist.py b/src/compas_blender/artists/sphereartist.py new file mode 100644 index 000000000000..d2bebb4c965f --- /dev/null +++ b/src/compas_blender/artists/sphereartist.py @@ -0,0 +1,54 @@ +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, u=None, v=None): + """Draw the sphere associated with the artist. + + Parameters + ---------- + 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 + vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) + objects = [] + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) + objects.append(obj) + self.objects += objects + return objects diff --git a/src/compas_blender/artists/volmeshartist.py b/src/compas_blender/artists/volmeshartist.py index 80001db47a0e..ba74bd4aece7 100644 --- a/src/compas_blender/artists/volmeshartist.py +++ b/src/compas_blender/artists/volmeshartist.py @@ -3,6 +3,7 @@ from typing import Any import bpy + from compas.artists import MeshArtist from .artist import BlenderArtist From 81445430f0465d97b9e8463054a1d635c64577d0 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 11:46:56 +0200 Subject: [PATCH 36/71] add capsule --- src/compas_blender/artists/capsuleartist.py | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/compas_blender/artists/capsuleartist.py diff --git a/src/compas_blender/artists/capsuleartist.py b/src/compas_blender/artists/capsuleartist.py new file mode 100644 index 000000000000..d2bebb4c965f --- /dev/null +++ b/src/compas_blender/artists/capsuleartist.py @@ -0,0 +1,54 @@ +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, u=None, v=None): + """Draw the sphere associated with the artist. + + Parameters + ---------- + 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 + vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) + objects = [] + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) + objects.append(obj) + self.objects += objects + return objects From c81a9e63a81d26bc245613017f08db04107bda4f Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 11:47:03 +0200 Subject: [PATCH 37/71] use same function --- src/compas_blender/artists/boxartist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/compas_blender/artists/boxartist.py b/src/compas_blender/artists/boxartist.py index 2cc8840cafa1..43eb969ac9b7 100644 --- a/src/compas_blender/artists/boxartist.py +++ b/src/compas_blender/artists/boxartist.py @@ -34,8 +34,7 @@ def draw(self): list The objects created in Blender. """ - vertices = self.shape.vertices - faces = self.shape.faces + vertices, faces = self.shape.to_vertices_and_faces() objects = [] obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) objects.append(obj) From ae60faeb6ab073f6daf9272f010dec39ead3c9e5 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 11:47:09 +0200 Subject: [PATCH 38/71] update docs --- src/compas_blender/artists/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index eb8b80ad8034..0c84f4ebeb43 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -23,10 +23,13 @@ .. autosummary:: :toctree: generated/ + BoxArtist + CapsuleArtist FrameArtist NetworkArtist MeshArtist RobotModelArtist + SphereArtist """ import inspect @@ -36,6 +39,7 @@ from compas.artists import DataArtistNotRegistered from compas.geometry import Box +from compas.geometry import Capsule from compas.geometry import Frame from compas.geometry import Sphere from compas.datastructures import Mesh @@ -44,6 +48,7 @@ from .artist import BlenderArtist # noqa: F401 from .boxartist import BoxArtist +from .capsuleartist import CapsuleArtist from .frameartist import FrameArtist from .meshartist import MeshArtist from .networkartist import NetworkArtist @@ -52,6 +57,7 @@ Artist.register(Box, BoxArtist) +Artist.register(Capsule, CapsuleArtist) Artist.register(Frame, FrameArtist) Artist.register(Mesh, MeshArtist) Artist.register(Network, NetworkArtist) From 9157edf8bd8184e31e389a52b43c92c1877ec91d Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 12:00:17 +0200 Subject: [PATCH 39/71] capsule, cone, cylinder, polyhedron --- src/compas_blender/artists/__init__.py | 18 ++++++- src/compas_blender/artists/capsuleartist.py | 20 +++---- src/compas_blender/artists/coneartist.py | 50 +++++++++++++++++ src/compas_blender/artists/cylinderartist.py | 54 +++++++++++++++++++ .../artists/polyhedronartist.py | 42 +++++++++++++++ 5 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 src/compas_blender/artists/coneartist.py create mode 100644 src/compas_blender/artists/cylinderartist.py create mode 100644 src/compas_blender/artists/polyhedronartist.py diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 0c84f4ebeb43..0e18964c5c83 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -25,9 +25,12 @@ BoxArtist CapsuleArtist + ConeArtist + CylinderArtist FrameArtist NetworkArtist MeshArtist + PolyhedronArtist RobotModelArtist SphereArtist @@ -40,7 +43,10 @@ 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.datastructures import Mesh from compas.datastructures import Network @@ -49,18 +55,24 @@ from .artist import BlenderArtist # noqa: F401 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 .polyhedronartist import PolyhedronArtist from .robotmodelartist import RobotModelArtist from .sphereartist import SphereArtist Artist.register(Box, BoxArtist) Artist.register(Capsule, CapsuleArtist) +Artist.register(Cone, ConeArtist) +Artist.register(Cylinder, CylinderArtist) Artist.register(Frame, FrameArtist) Artist.register(Mesh, MeshArtist) Artist.register(Network, NetworkArtist) +Artist.register(Polyhedron, PolyhedronArtist) Artist.register(RobotModel, RobotModelArtist) Artist.register(Sphere, SphereArtist) @@ -81,9 +93,13 @@ def new_artist_blender(cls, *args, **kwargs): __all__ = [ 'BoxArtist', + 'CapsuleArtist', + 'ConeArtist', + 'CylinderArtist', 'FrameArtist', - 'NetworkArtist', 'MeshArtist', + 'NetworkArtist', + 'PolyhedronArtist', 'RobotModelArtist', 'SphereArtist', ] diff --git a/src/compas_blender/artists/capsuleartist.py b/src/compas_blender/artists/capsuleartist.py index d2bebb4c965f..e735474a1173 100644 --- a/src/compas_blender/artists/capsuleartist.py +++ b/src/compas_blender/artists/capsuleartist.py @@ -5,39 +5,39 @@ import bpy import compas_blender -from compas.geometry import Sphere +from compas.geometry import Capsule from compas.artists import ShapeArtist from .artist import BlenderArtist -class SphereArtist(BlenderArtist, ShapeArtist): - """Artist for drawing sphere shapes. +class CapsuleArtist(BlenderArtist, ShapeArtist): + """Artist for drawing capsule shapes. Parameters ---------- - sphere : :class:`compas.geometry.Sphere` - A COMPAS sphere. + 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, - sphere: Sphere, + capsule: Capsule, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): - super().__init__(shape=sphere, collection=collection or sphere.name, **kwargs) + super().__init__(shape=capsule, collection=collection or capsule.name, **kwargs) def draw(self, u=None, v=None): - """Draw the sphere associated with the artist. + """Draw the capsule associated with the artist. Parameters ---------- u : int, optional Number of faces in the "u" direction. - Default is ``~SphereArtist.u``. + Default is ``~CapsuleArtist.u``. v : int, optional Number of faces in the "v" direction. - Default is ``~SphereArtist.v``. + Default is ``~CapsuleArtist.v``. Returns ------- diff --git a/src/compas_blender/artists/coneartist.py b/src/compas_blender/artists/coneartist.py new file mode 100644 index 000000000000..7b11323c9de1 --- /dev/null +++ b/src/compas_blender/artists/coneartist.py @@ -0,0 +1,50 @@ +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, u=None): + """Draw the cone associated with the artist. + + Parameters + ---------- + 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 + vertices, faces = self.shape.to_vertices_and_faces(u=u) + objects = [] + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) + objects.append(obj) + self.objects += objects + return objects diff --git a/src/compas_blender/artists/cylinderartist.py b/src/compas_blender/artists/cylinderartist.py new file mode 100644 index 000000000000..ac6955f02320 --- /dev/null +++ b/src/compas_blender/artists/cylinderartist.py @@ -0,0 +1,54 @@ +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, u=None, v=None): + """Draw the cylinder associated with the artist. + + Parameters + ---------- + u : int, optional + Number of faces in the "u" direction. + Default is ``~CylinderArtist.u``. + v : int, optional + Number of faces in the "v" direction. + Default is ``~CylinderArtist.v``. + + Returns + ------- + list + The objects created in Blender. + """ + u = u or self.u + v = v or self.v + vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) + objects = [] + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) + objects.append(obj) + self.objects += objects + return objects diff --git a/src/compas_blender/artists/polyhedronartist.py b/src/compas_blender/artists/polyhedronartist.py new file mode 100644 index 000000000000..a3d61e05901e --- /dev/null +++ b/src/compas_blender/artists/polyhedronartist.py @@ -0,0 +1,42 @@ +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): + """Draw the polyhedron associated with the artist. + + Returns + ------- + list + The objects created in Blender. + """ + vertices, faces = self.shape.to_vertices_and_faces() + objects = [] + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) + objects.append(obj) + self.objects += objects + return objects From db3735c86584f3e391d372b076b3a904f866c836 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 12:02:53 +0200 Subject: [PATCH 40/71] torus artist --- src/compas_blender/artists/__init__.py | 4 ++ src/compas_blender/artists/torusartist.py | 54 +++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/compas_blender/artists/torusartist.py diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 0e18964c5c83..ea2c306acb3d 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -48,6 +48,7 @@ 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 @@ -63,6 +64,7 @@ from .polyhedronartist import PolyhedronArtist from .robotmodelartist import RobotModelArtist from .sphereartist import SphereArtist +from .torusartist import TorusArtist Artist.register(Box, BoxArtist) @@ -75,6 +77,7 @@ Artist.register(Polyhedron, PolyhedronArtist) Artist.register(RobotModel, RobotModelArtist) Artist.register(Sphere, SphereArtist) +Artist.register(Torus, TorusArtist) @plugin(category='factories', pluggable_name='new_artist', requires=['bpy']) @@ -102,4 +105,5 @@ def new_artist_blender(cls, *args, **kwargs): 'PolyhedronArtist', 'RobotModelArtist', 'SphereArtist', + 'TorusArtist', ] diff --git a/src/compas_blender/artists/torusartist.py b/src/compas_blender/artists/torusartist.py new file mode 100644 index 000000000000..611c31af27ee --- /dev/null +++ b/src/compas_blender/artists/torusartist.py @@ -0,0 +1,54 @@ +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, u=None, v=None): + """Draw the torus associated with the artist. + + Parameters + ---------- + 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 + vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) + objects = [] + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) + objects.append(obj) + self.objects += objects + return objects From 2d735e9dc83aa93c14168f9595defefe991821f1 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 12:04:57 +0200 Subject: [PATCH 41/71] add note about abc --- src/compas_rhino/artists/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 71f5d97fc777..59939691605d 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -157,6 +157,7 @@ def new_artist_rhino(cls, *args, **kwargs): dtype = type(data) if dtype not in Artist.ITEM_ARTIST: raise DataArtistNotRegistered('No Rhino artist is registered for this data type: {}'.format(dtype)) + # TODO: move this to the plugin module and/or to a dedicated function cls = Artist.ITEM_ARTIST[dtype] for name, value in inspect.getmembers(cls): if inspect.ismethod(value): From 479e509d14f61d3d82d608be0f95677e923fd10e Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 13:28:15 +0200 Subject: [PATCH 42/71] convert ghpython artists --- src/compas_blender/artists/__init__.py | 3 +- src/compas_ghpython/artists/__init__.py | 30 +--- src/compas_ghpython/artists/_artist.py | 17 --- .../artists/_primitiveartist.py | 44 ------ src/compas_ghpython/artists/_shapeartist.py | 56 ------- src/compas_ghpython/artists/artist.py | 13 ++ src/compas_ghpython/artists/circleartist.py | 24 ++- src/compas_ghpython/artists/frameartist.py | 34 ++--- src/compas_ghpython/artists/lineartist.py | 17 +-- src/compas_ghpython/artists/meshartist.py | 129 ++++++---------- src/compas_ghpython/artists/networkartist.py | 68 +++------ src/compas_ghpython/artists/pointartist.py | 18 +-- src/compas_ghpython/artists/polylineartist.py | 17 +-- .../artists/robotmodelartist.py | 21 +-- src/compas_ghpython/artists/volmeshartist.py | 141 +++++++----------- src/compas_rhino/artists/meshartist.py | 6 +- src/compas_rhino/artists/robotmodelartist.py | 2 +- src/compas_rhino/artists/volmeshartist.py | 6 +- 18 files changed, 212 insertions(+), 434 deletions(-) delete mode 100644 src/compas_ghpython/artists/_artist.py delete mode 100644 src/compas_ghpython/artists/_primitiveartist.py delete mode 100644 src/compas_ghpython/artists/_shapeartist.py create mode 100644 src/compas_ghpython/artists/artist.py diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index ea2c306acb3d..752863a1a920 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -53,7 +53,7 @@ from compas.datastructures import Network from compas.robots import RobotModel -from .artist import BlenderArtist # noqa: F401 +from .artist import BlenderArtist from .boxartist import BoxArtist from .capsuleartist import CapsuleArtist from .coneartist import ConeArtist @@ -95,6 +95,7 @@ def new_artist_blender(cls, *args, **kwargs): __all__ = [ + 'BlenderArtist', 'BoxArtist', 'CapsuleArtist', 'ConeArtist', diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index d71bafb9d12f..a92cfb928f28 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -5,16 +5,16 @@ .. currentmodule:: compas_ghpython.artists -.. rst-class:: lead -Artists for visualising (painting) COMPAS objects with GHPython. -Artists convert COMPAS objects to Rhino geometry and data. +Base Classes +============ -.. code-block:: python +.. autosummary:: + :toctree: generated/ + :nosignatures: - pass + GHArtist ----- Geometry Artists ================ @@ -51,24 +51,10 @@ RobotModelArtist - -Base Classes -============ - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - BaseArtist - PrimitiveArtist - ShapeArtist - """ from __future__ import absolute_import -from ._artist import BaseArtist -from ._primitiveartist import PrimitiveArtist -from ._shapeartist import ShapeArtist +from .artist import GHArtist from .circleartist import CircleArtist from .frameartist import FrameArtist @@ -83,7 +69,7 @@ from .robotmodelartist import RobotModelArtist __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..23c1509d16b1 100644 --- a/src/compas_ghpython/artists/circleartist.py +++ b/src/compas_ghpython/artists/circleartist.py @@ -3,26 +3,22 @@ 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. @@ -31,12 +27,12 @@ def draw(self): :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..ad10adf66b68 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,21 @@ 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) diff --git a/src/compas_ghpython/artists/lineartist.py b/src/compas_ghpython/artists/lineartist.py index 148376737298..8eecdd935edf 100644 --- a/src/compas_ghpython/artists/lineartist.py +++ b/src/compas_ghpython/artists/lineartist.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__ = ['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. diff --git a/src/compas_ghpython/artists/meshartist.py b/src/compas_ghpython/artists/meshartist.py index 1e62cd94fc11..6ac2720b1d8d 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,24 @@ 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) diff --git a/src/compas_ghpython/artists/networkartist.py b/src/compas_ghpython/artists/networkartist.py index f8db95471144..755106bba424 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. @@ -79,14 +52,16 @@ def draw_nodes(self, nodes=None, color=None): 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 +72,24 @@ 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..975c99ba8be0 100644 --- a/src/compas_ghpython/artists/pointartist.py +++ b/src/compas_ghpython/artists/pointartist.py @@ -3,26 +3,22 @@ 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. 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..3ef986c55be4 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 BaseRobotModelArtist +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, BaseRobotModelArtist): + """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..057fe7dca598 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,91 @@ 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_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index ef1be32784ed..f916e219511d 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -3,7 +3,6 @@ from __future__ import division from functools import partial -import compas_rhino from compas.utilities import color_to_colordict from compas.utilities import pairwise @@ -12,6 +11,7 @@ from compas.geometry import centroid_polygon from compas.geometry import centroid_points +import compas_rhino from compas.artists import MeshArtist from .artist import RhinoArtist @@ -118,8 +118,6 @@ def draw_mesh(self, color=None, 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) guid = compas_rhino.draw_mesh(vertices, new_faces, layer=layer, name=name, color=color, disjoint=disjoint) @@ -130,7 +128,7 @@ 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 diff --git a/src/compas_rhino/artists/robotmodelartist.py b/src/compas_rhino/artists/robotmodelartist.py index f914727d1ce5..37cec24efcd7 100644 --- a/src/compas_rhino/artists/robotmodelartist.py +++ b/src/compas_rhino/artists/robotmodelartist.py @@ -21,7 +21,7 @@ class RobotModelArtist(RhinoArtist, BaseRobotModelArtist): - """Visualizer for robots inside a Rhino environment. + """Artist for drawing robot models. Parameters ---------- diff --git a/src/compas_rhino/artists/volmeshartist.py b/src/compas_rhino/artists/volmeshartist.py index 9509d4919a02..13961409613b 100644 --- a/src/compas_rhino/artists/volmeshartist.py +++ b/src/compas_rhino/artists/volmeshartist.py @@ -84,7 +84,7 @@ def draw_vertices(self, vertices=None, color=None): 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 @@ -114,7 +114,7 @@ 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 @@ -145,7 +145,7 @@ 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 From 89c7f87032de913dd3b456272d5be156b86d3554 Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 16:17:32 +0200 Subject: [PATCH 43/71] clean up init files --- src/compas_blender/artists/__init__.py | 1 + src/compas_ghpython/artists/__init__.py | 44 ++++++++++++++++++++++-- src/compas_rhino/artists/__init__.py | 45 +++++++++++-------------- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 752863a1a920..c62b27eccb44 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -86,6 +86,7 @@ def new_artist_blender(cls, *args, **kwargs): dtype = type(data) if dtype not in Artist.ITEM_ARTIST: raise DataArtistNotRegistered('No Blender artist is registered for this data type: {}'.format(dtype)) + # TODO: move this to the plugin module and/or to a dedicated function cls = Artist.ITEM_ARTIST[dtype] for name, value in inspect.getmembers(cls): if inspect.isfunction(value): diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index a92cfb928f28..68986e609c2e 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -54,20 +54,58 @@ """ from __future__ import absolute_import -from .artist import GHArtist +import inspect + +from compas.plugins import plugin +from compas.artists import Artist +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 .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 +Artist.register(Circle, CircleArtist) +Artist.register(Frame, FrameArtist) +Artist.register(Line, LineArtist) +Artist.register(Point, PointArtist) +Artist.register(Polyline, PolylineArtist) +Artist.register(Mesh, MeshArtist) +Artist.register(Network, NetworkArtist) +Artist.register(VolMesh, VolMeshArtist) + + +@plugin(category='factories', pluggable_name='new_artist', requires=['ghpythonlib']) +def new_artist_gh(cls, *args, **kwargs): + data = args[0] + dtype = type(data) + if dtype not in Artist.ITEM_ARTIST: + raise DataArtistNotRegistered('No GH artist is registered for this data type: {}'.format(dtype)) + # TODO: move this to the plugin module and/or to a dedicated function + cls = Artist.ITEM_ARTIST[dtype] + for name, value in inspect.getmembers(cls): + if inspect.ismethod(value): + if hasattr(value, '__isabstractmethod__'): + raise Exception('Abstract method not implemented') + return super(Artist, cls).__new__(cls) + + __all__ = [ 'GHArtist', 'PrimitiveArtist', diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 59939691605d..edb3f80b08e7 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -103,7 +103,6 @@ from compas.robots import RobotModel from .artist import RhinoArtist - from .circleartist import CircleArtist from .frameartist import FrameArtist from .lineartist import LineArtist @@ -112,7 +111,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 @@ -120,35 +118,30 @@ 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 -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) +Artist.register(Circle, CircleArtist) +Artist.register(Frame, FrameArtist) +Artist.register(Line, LineArtist) +Artist.register(Plane, PlaneArtist) +Artist.register(Point, PointArtist) +Artist.register(Polygon, PolygonArtist) +Artist.register(Polyline, PolylineArtist) +Artist.register(Vector, VectorArtist) +Artist.register(Box, BoxArtist) +Artist.register(Capsule, CapsuleArtist) +Artist.register(Cone, ConeArtist) +Artist.register(Cylinder, CylinderArtist) +Artist.register(Polyhedron, PolyhedronArtist) +Artist.register(Sphere, SphereArtist) +Artist.register(Torus, TorusArtist) +Artist.register(Mesh, MeshArtist) +Artist.register(Network, NetworkArtist) +Artist.register(VolMesh, VolMeshArtist) +Artist.register(RobotModel, RobotModelArtist) @plugin(category='factories', pluggable_name='new_artist', requires=['Rhino']) From 58671de2c322b8b1f553caf155aee79319ff5b1a Mon Sep 17 00:00:00 2001 From: brgcode Date: Wed, 22 Sep 2021 16:22:21 +0200 Subject: [PATCH 44/71] rename bae robotmodelartist --- src/compas/artists/__init__.py | 8 +++++--- src/compas/artists/robotmodelartist.py | 4 ++-- src/compas_blender/artists/robotmodelartist.py | 4 ++-- src/compas_ghpython/artists/robotmodelartist.py | 4 ++-- src/compas_rhino/artists/robotmodelartist.py | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/compas/artists/__init__.py b/src/compas/artists/__init__.py index eb8d20fa0431..20b6470ca1ee 100644 --- a/src/compas/artists/__init__.py +++ b/src/compas/artists/__init__.py @@ -13,7 +13,7 @@ :nosignatures: Artist - BaseRobotModelArtist + RobotModelArtist MeshArtist NetworkArtist PrimitiveArtist @@ -40,17 +40,19 @@ from .meshartist import MeshArtist from .networkartist import NetworkArtist from .primitiveartist import PrimitiveArtist -from .robotmodelartist import BaseRobotModelArtist +from .robotmodelartist import RobotModelArtist from .shapeartist import ShapeArtist from .volmeshartist import VolMeshArtist +BaseRobotModelArtist = RobotModelArtist + __all__ = [ 'DataArtistNotRegistered', 'Artist', 'MeshArtist', 'NetworkArtist', 'PrimitiveArtist', - 'BaseRobotModelArtist', + 'RobotModelArtist', 'ShapeArtist', 'VolMeshArtist', ] diff --git a/src/compas/artists/robotmodelartist.py b/src/compas/artists/robotmodelartist.py index 0b2475d9e949..9dbf4997de4a 100644 --- a/src/compas/artists/robotmodelartist.py +++ b/src/compas/artists/robotmodelartist.py @@ -49,7 +49,7 @@ def create_geometry(self, geometry, name=None, color=None): raise NotImplementedError -class BaseRobotModelArtist(AbstractRobotModelArtist, Artist): +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 @@ -69,7 +69,7 @@ class BaseRobotModelArtist(AbstractRobotModelArtist, Artist): """ 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_blender/artists/robotmodelartist.py b/src/compas_blender/artists/robotmodelartist.py index 5e113cd01955..f5167e0ad35d 100644 --- a/src/compas_blender/artists/robotmodelartist.py +++ b/src/compas_blender/artists/robotmodelartist.py @@ -10,11 +10,11 @@ from compas.datastructures import Mesh from compas.geometry import Transformation, Shape from compas.robots import RobotModel -from compas.artists import BaseRobotModelArtist +from compas.artists import RobotModelArtist from .artist import BlenderArtist -class RobotModelArtist(BlenderArtist, BaseRobotModelArtist): +class RobotModelArtist(BlenderArtist, RobotModelArtist): """Visualizer for robot models inside a Blender environment. Parameters diff --git a/src/compas_ghpython/artists/robotmodelartist.py b/src/compas_ghpython/artists/robotmodelartist.py index 3ef986c55be4..fdc1e0690932 100644 --- a/src/compas_ghpython/artists/robotmodelartist.py +++ b/src/compas_ghpython/artists/robotmodelartist.py @@ -6,12 +6,12 @@ from compas_rhino.geometry.transformations import xtransform -from compas.artists import BaseRobotModelArtist +from compas.artists import RobotModelArtist from compas_ghpython.utilities import draw_mesh from .artist import GHArtist -class RobotModelArtist(GHArtist, BaseRobotModelArtist): +class RobotModelArtist(GHArtist, RobotModelArtist): """Artist for drawing robot models. Parameters diff --git a/src/compas_rhino/artists/robotmodelartist.py b/src/compas_rhino/artists/robotmodelartist.py index 37cec24efcd7..48267ae92ee8 100644 --- a/src/compas_rhino/artists/robotmodelartist.py +++ b/src/compas_rhino/artists/robotmodelartist.py @@ -13,14 +13,14 @@ from compas.geometry import centroid_polygon from compas.utilities import pairwise -from compas.artists import BaseRobotModelArtist +from compas.artists import RobotModelArtist import compas_rhino from compas_rhino.artists import RhinoArtist from compas_rhino.geometry.transformations import xform_from_transformation -class RobotModelArtist(RhinoArtist, BaseRobotModelArtist): +class RobotModelArtist(RhinoArtist, RobotModelArtist): """Artist for drawing robot models. Parameters From 48ba7933ea0424a0d82802b12b3ef53fc5ab358b Mon Sep 17 00:00:00 2001 From: brgcode Date: Sat, 25 Sep 2021 20:12:41 +0200 Subject: [PATCH 45/71] rebase plotter artists --- docs/_images/tutorial/plotters_dynamic.gif | Bin 129242 -> 138721 bytes .../tutorial/plotters_line-options.png | Bin 129823 -> 98385 bytes .../tutorial/plotters_vector-options.png | Bin 63369 -> 14190 bytes docs/tutorial/plotters_line-options.py | 3 +- docs/tutorial/plotters_point-options.py | 3 +- docs/tutorial/plotters_polygon-options.py | 3 +- docs/tutorial/plotters_vector-options.py | 1 + src/compas/artists/artist.py | 5 + src/compas_plotters/__init__.py | 59 +- src/compas_plotters/_plotter.py | 609 ------------------ src/compas_plotters/artists/__init__.py | 27 +- src/compas_plotters/artists/artist.py | 37 +- src/compas_plotters/artists/circleartist.py | 30 +- src/compas_plotters/artists/ellipseartist.py | 30 +- src/compas_plotters/artists/lineartist.py | 29 +- src/compas_plotters/artists/meshartist.py | 254 ++++---- src/compas_plotters/artists/networkartist.py | 10 +- src/compas_plotters/artists/pointartist.py | 30 +- src/compas_plotters/artists/polygonartist.py | 27 +- src/compas_plotters/artists/polylineartist.py | 27 +- src/compas_plotters/artists/segmentartist.py | 27 +- src/compas_plotters/artists/vectorartist.py | 28 +- src/compas_plotters/geometryplotter.py | 335 ---------- src/compas_plotters/meshplotter.py | 349 ---------- src/compas_plotters/networkplotter.py | 275 -------- src/compas_plotters/plotter.py | 15 +- src/compas_rhino/artists/meshartist.py | 6 +- 27 files changed, 366 insertions(+), 1853 deletions(-) delete mode 100644 src/compas_plotters/_plotter.py delete mode 100644 src/compas_plotters/geometryplotter.py delete mode 100644 src/compas_plotters/meshplotter.py delete mode 100644 src/compas_plotters/networkplotter.py diff --git a/docs/_images/tutorial/plotters_dynamic.gif b/docs/_images/tutorial/plotters_dynamic.gif index edff60f5cd9655df719b21c747579a591118b087..f04f5d41c674ec80ed646342dd9e800f29ee2166 100644 GIT binary patch literal 138721 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

EDZ&2y&{i&2c+fM$RFa8A;umkq*T;;#} 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 bBTmkSNLy90fKwUf$q3pQ8MP!RpDq7C_FQ@` diff --git a/docs/_images/tutorial/plotters_line-options.png b/docs/_images/tutorial/plotters_line-options.png index a64f76f5b0287a860d7844ebf0a4e98e775ca2d2..57df6fad80bd159727cc7f716e23e1afb5f116d9 100644 GIT binary patch literal 98385 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

}xm9sB0hB{2>Z|UrzT>DcYtdwepICAip{Tu!oiM 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 XWm#Bww20Rx_=i5BcRcr)P3Zpv1gTyl diff --git a/docs/_images/tutorial/plotters_vector-options.png b/docs/_images/tutorial/plotters_vector-options.png index f0e719f64d5375f372a726565b968501fede02c0..d24b97034c39ecbadc19420eac9eff2b30e292c4 100644 GIT binary patch literal 14190 zcmeI2ze@sP7{?#eOeM)^(I7B_mg1C(z>-oqNuwmH!IscKO|VY%P=|)2TpC-Vp&^LY z5c&hW_F5VOaR^#kg0_APMen_D_BRxK59A(>^SJx+e7?`~KD>u3i3O))&_P7bcuZa; z>d}b0&umt9rFpd3VyA!_O{!~3R;{K>8Cp)O#e$+1@>||Yrc}-=yOYwCG%@boR@Gv8 z#^>Aly+Kk+Ip5g9W0F0@UW{#&iKG$ZXR2MySBR$DaXGwRJAH0lJ9V#1Z`^%W=Z-@W z`PK4~yS>?Ce>0BYOLIs>q5r*ofUJ<#{WEPR^)Lh>#jYzzMElxFXDvP*7MNpl%Y{144ViP(XWt_P{8Oe|S-0d#LpV{eh=h<2~ci KgnSuE?SBI@)EoH# literal 63369 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 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/artists/artist.py b/src/compas/artists/artist.py index 2b649ce85d1e..2371e0ad1343 100644 --- a/src/compas/artists/artist.py +++ b/src/compas/artists/artist.py @@ -44,6 +44,11 @@ def build(item, **kwargs): artist = artist_type(item, **kwargs) return artist + @staticmethod + def build_as(item, artist_type, **kwargs): + artist = artist_type(item, **kwargs) + return artist + @abstractmethod def draw(self): raise NotImplementedError diff --git a/src/compas_plotters/__init__.py b/src/compas_plotters/__init__.py index 19733bbfc79c..05edd23b9ff7 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 .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..a265bc645071 100644 --- a/src/compas_plotters/artists/__init__.py +++ b/src/compas_plotters/artists/__init__.py @@ -29,6 +29,12 @@ NetworkArtist """ +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 +46,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,7 +54,6 @@ from .polygonartist import PolygonArtist from .circleartist import CircleArtist from .ellipseartist import EllipseArtist - from .meshartist import MeshArtist from .networkartist import NetworkArtist @@ -59,13 +64,27 @@ Artist.register(Polygon, PolygonArtist) Artist.register(Circle, CircleArtist) Artist.register(Ellipse, EllipseArtist) - Artist.register(Mesh, MeshArtist) Artist.register(Network, NetworkArtist) +@plugin(category='factories', pluggable_name='new_artist', trylast=True, requires=['matplotlib']) +def new_artist_plotter(cls, *args, **kwargs): + data = args[0] + dtype = type(data) + if dtype not in Artist.ITEM_ARTIST: + raise DataArtistNotRegistered('No Plotter artist is registered for this data type: {}'.format(dtype)) + # TODO: move this to the plugin module and/or to a dedicated function + cls = Artist.ITEM_ARTIST[dtype] + 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..1dd4e6b4ce7c 100644 --- a/src/compas_plotters/artists/artist.py +++ b/src/compas_plotters/artists/artist.py @@ -1,31 +1,14 @@ -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): + def __init__(self, **kwargs): + super().__init__(**kwargs) self.plotter = None - self.item = item - - @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 def viewbox(self): xlim = self.plotter.axes.get_xlim() @@ -38,13 +21,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..c239f9653c17 100644 --- a/src/compas_plotters/artists/circleartist.py +++ b/src/compas_plotters/artists/circleartist.py @@ -1,13 +1,18 @@ -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 @@ -19,10 +24,12 @@ 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, + **kwargs: Any): + + super().__init__(primitive=circle, **kwargs) + self._mpl_circle = None - self.circle = circle self.linewidth = linewidth self.linestyle = linestyle self.facecolor = facecolor @@ -30,6 +37,14 @@ def __init__(self, self.fill = fill self.alpha = alpha + @property + def circle(self): + return self.primitive + + @circle.setter + def circle(self, circle): + self.primitive = circle + @property def data(self) -> List[List[float]]: points = [ @@ -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..5acd0fbb09d8 100644 --- a/src/compas_plotters/artists/ellipseartist.py +++ b/src/compas_plotters/artists/ellipseartist.py @@ -1,13 +1,18 @@ -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 @@ -19,10 +24,12 @@ 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, + **kwargs: Any): + + super().__init__(primitive=ellipse, **kwargs) + self._mpl_ellipse = None - self.ellipse = ellipse self.linewidth = linewidth self.linestyle = linestyle self.facecolor = facecolor @@ -30,6 +37,14 @@ def __init__(self, self.fill = fill self.alpha = alpha + @property + def ellipse(self): + return self.primitive + + @ellipse.setter + def ellipse(self, ellipse): + self.primitive = ellipse + @property def data(self) -> List[List[float]]: points = [ @@ -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..13c00ab62557 100644 --- a/src/compas_plotters/artists/lineartist.py +++ b/src/compas_plotters/artists/lineartist.py @@ -1,14 +1,19 @@ -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 @@ -19,19 +24,29 @@ def __init__(self, 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), + **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 + @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.""" xlim, ylim = self.plotter.viewbox @@ -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..7c26377e6de3 100644 --- a/src/compas_plotters/artists/meshartist.py +++ b/src/compas_plotters/artists/meshartist.py @@ -1,15 +1,21 @@ -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): +class MeshArtist(PlotterArtist, MeshArtist): """Artist for COMPAS mesh data structures.""" default_vertexcolor: Color = (1, 1, 1) @@ -28,160 +34,164 @@ def __init__(self, show_vertices: bool = True, show_edges: bool = True, show_faces: bool = True, + vertices: Optional[List[int]] = None, + edges: Optional[List[int]] = None, + faces: Optional[List[int]] = None, 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) + facecolor: Color = (0.9, 0.9, 0.9), + **kwargs: Any): + + super().__init__(mesh=mesh, **kwargs) + 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 + self._edge_width = None + self.vertices = vertices + self.edges = edges + self.faces = faces 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 - - @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 = {} + self.vertex_color = vertexcolor + self.edge_color = edgecolor + self.face_color = facecolor @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 item(self): + return self.mesh - @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 = {} + @item.setter + def item(self, item: Mesh): + self.mesh = item @property - def edgewidth(self) -> Dict[Tuple[int, int], float]: + def edge_width(self) -> Dict[Tuple[int, int], float]: """dict: Edge widths.""" - return self._edgewidth + if not self._edge_width: + self._edge_width = {edge: self.default_edgewidth for edge in self.mesh.edges()} + return self._edge_width - @edgewidth.setter - def edgewidth(self, edgewidth: Union[float, Dict[Tuple[int, int], float]]): + @edge_width.setter + def edge_width(self, edgewidth: Union[float, Dict[Tuple[int, int], float]]): if isinstance(edgewidth, dict): - self._edgewidth = edgewidth + self._edge_width = edgewidth elif isinstance(edgewidth, (int, float)): - self._edgewidth = {edge: edgewidth for edge in self.mesh.edges()} - else: - self._edgewidth = {} + self._edge_width = {edge: edgewidth for edge in self.mesh.edges()} @property def data(self) -> List[List[float]]: return self.mesh.vertices_attributes('xy') - def draw(self) -> None: + def draw(self, + vertices=None, + edges=None, + faces=None, + vertexcolor=None, + edgecolor=None, + facecolor=None) -> None: """Draw the mesh.""" - vertex_xy = {vertex: self.mesh.vertex_attributes(vertex, 'xy') for vertex in self.mesh.vertices()} - 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 + self.draw_faces(faces=faces, color=facecolor) 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 + self.draw_edges(edges=edges, color=edgecolor) 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) + + def draw_vertices(self, vertices=None, color=None, text=None): + if vertices: + self.vertices = vertices + if color: + self.vertex_color = color + + if self.sizepolicy == 'absolute': + size = self.vertexsize / self.plotter.dpi + else: + size = self.vertexsize / self.mesh.number_of_vertices() + + circles = [] + for vertex in self.vertices: + x, y = self.vertex_xyz[vertex][:2] + circle = Circle( + [x, y], + radius=size, + 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._mpl_vertex_collection = collection + + def draw_edges(self, edges=None, color=None, text=None): + 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._mpl_edge_collection = collection + + def draw_faces(self, faces=None, color=None, text=None): + 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) + + 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 redraw(self) -> None: raise NotImplementedError diff --git a/src/compas_plotters/artists/networkartist.py b/src/compas_plotters/artists/networkartist.py index 4e4f8c2e3272..32917268ec21 100644 --- a/src/compas_plotters/artists/networkartist.py +++ b/src/compas_plotters/artists/networkartist.py @@ -3,13 +3,13 @@ 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): - """""" +class NetworkArtist(PlotterArtist): + """Artist for COMPAS network data structures.""" default_nodecolor: Color = (1, 1, 1) default_edgecolor: Color = (0, 0, 0) @@ -29,7 +29,9 @@ def __init__(self, nodecolor: Color = (1, 1, 1), edgewidth: float = 1.0, edgecolor: Color = (0, 0, 0)): - super(NetworkArtist, self).__init__(network) + + super().__init__(network) + self._mpl_node_collection = None self._mpl_edge_collection = None self._nodecolor = None diff --git a/src/compas_plotters/artists/pointartist.py b/src/compas_plotters/artists/pointartist.py index 3efc29442a94..5cd1ac55f095 100644 --- a/src/compas_plotters/artists/pointartist.py +++ b/src/compas_plotters/artists/pointartist.py @@ -1,13 +1,18 @@ -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 @@ -16,15 +21,25 @@ 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), + **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 + @property + def point(self): + return self.primitive + + @point.setter + def point(self, point): + self.primitive = point + @property def _T(self): F = self.plotter.figure.dpi_scale_trans @@ -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..da4bf67fb30f 100644 --- a/src/compas_plotters/artists/polygonartist.py +++ b/src/compas_plotters/artists/polygonartist.py @@ -1,13 +1,18 @@ -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 @@ -19,10 +24,12 @@ 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, + **kwargs: Any): + + super().__init__(primitive=polygon, **kwargs) + self._mpl_polygon = None - self.polygon = polygon self.linewidth = linewidth self.linestyle = linestyle self.facecolor = facecolor @@ -30,6 +37,14 @@ def __init__(self, self.fill = fill self.alpha = alpha + @property + def polygon(self): + return self.primitive + + @polygon.setter + def polygon(self, polygon): + self.primitive = polygon + @property def data(self) -> List[List[float]]: return [point[:2] for point in self.polygon.points] diff --git a/src/compas_plotters/artists/polylineartist.py b/src/compas_plotters/artists/polylineartist.py index dd123c6ed97f..6a96d2430516 100644 --- a/src/compas_plotters/artists/polylineartist.py +++ b/src/compas_plotters/artists/polylineartist.py @@ -1,13 +1,18 @@ -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 @@ -17,16 +22,26 @@ def __init__(self, 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), + **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 + @property + def polyline(self): + return self.primitive + + @polyline.setter + def polyline(self, polyline): + self.primitive = polyline + @property def data(self) -> List[List[float]]: return [point[:2] for point in self.polyline.points] diff --git a/src/compas_plotters/artists/segmentartist.py b/src/compas_plotters/artists/segmentartist.py index 73b85af8342f..832e4bb9f145 100644 --- a/src/compas_plotters/artists/segmentartist.py +++ b/src/compas_plotters/artists/segmentartist.py @@ -1,13 +1,18 @@ -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 @@ -17,17 +22,27 @@ def __init__(self, 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), + **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 + @property + def line(self): + return self.primitive + + @line.setter + def line(self, line): + self.primitive = line + @property def data(self) -> List[List[float]]: return [self.line.start[:2], self.line.end[:2]] diff --git a/src/compas_plotters/artists/vectorartist.py b/src/compas_plotters/artists/vectorartist.py index 62cf93b24767..02bfb46433d3 100644 --- a/src/compas_plotters/artists/vectorartist.py +++ b/src/compas_plotters/artists/vectorartist.py @@ -1,13 +1,19 @@ -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 @@ -16,15 +22,25 @@ 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), + **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 + @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]] 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..6365433fcd0f 100644 --- a/src/compas_plotters/plotter.py +++ b/src/compas_plotters/plotter.py @@ -6,7 +6,7 @@ from PIL import Image import compas -from compas_plotters import Artist +from .artists import Artist class Plotter: @@ -221,14 +221,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, diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index f916e219511d..ffe365d472af 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -142,7 +142,7 @@ def draw_vertices(self, vertices=None, color=None): """ self.vertex_color = color - vertices = vertices or list(self.mesh.vertices()) + vertices = vertices or self.vertices vertex_xyz = self.vertex_xyz points = [] for vertex in vertices: @@ -175,7 +175,7 @@ def draw_faces(self, faces=None, color=None, join_faces=False): """ self.face_color = color - faces = faces or list(self.mesh.faces()) + faces = faces or self.faces vertex_xyz = self.vertex_xyz facets = [] for face in faces: @@ -212,7 +212,7 @@ def draw_edges(self, edges=None, color=None): """ self.edge_color = color - edges = edges or list(self.mesh.edges()) + edges = edges or self.edges vertex_xyz = self.vertex_xyz lines = [] for edge in edges: From bde413fdbbee3a33a53e089b7d3724337c59ac72 Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 28 Sep 2021 00:58:18 +0200 Subject: [PATCH 46/71] lazy registration is necessary to avoid overwriting item-artist assignments --- src/compas/artists/meshartist.py | 82 ++--- src/compas/artists/networkartist.py | 10 +- src/compas/artists/shapeartist.py | 2 +- src/compas/artists/volmeshartist.py | 22 +- src/compas_blender/__init__.py | 2 +- src/compas_blender/artists/__init__.py | 34 +- src/compas_blender/artists/artist.py | 10 +- src/compas_blender/artists/boxartist.py | 6 +- src/compas_blender/artists/capsuleartist.py | 6 +- src/compas_blender/artists/coneartist.py | 6 +- src/compas_blender/artists/cylinderartist.py | 6 +- src/compas_blender/artists/frameartist.py | 10 +- src/compas_blender/artists/meshartist.py | 226 ++++++++------ src/compas_blender/artists/networkartist.py | 118 ++++--- .../artists/polyhedronartist.py | 6 +- src/compas_blender/artists/sphereartist.py | 6 +- src/compas_blender/artists/torusartist.py | 6 +- src/compas_blender/artists/volmeshartist.py | 1 + src/compas_ghpython/artists/__init__.py | 40 ++- src/compas_ghpython/artists/circleartist.py | 1 - src/compas_ghpython/artists/frameartist.py | 4 - src/compas_ghpython/artists/lineartist.py | 2 - src/compas_ghpython/artists/networkartist.py | 2 - src/compas_ghpython/artists/pointartist.py | 1 - src/compas_ghpython/artists/volmeshartist.py | 3 - src/compas_plotters/artists/__init__.py | 27 +- src/compas_plotters/artists/meshartist.py | 220 ++++++++++--- src/compas_plotters/artists/networkartist.py | 292 ++++++++++++------ src/compas_plotters/plotter.py | 24 +- src/compas_rhino/artists/__init__.py | 62 ++-- src/compas_rhino/artists/meshartist.py | 245 ++++++++++----- src/compas_rhino/artists/networkartist.py | 80 ++++- 32 files changed, 1033 insertions(+), 529 deletions(-) diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index d2306f1397ae..a7ee56747fad 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -61,9 +61,9 @@ class MeshArtist(Artist): """ default_color = (0, 0, 0) - default_vertexcolor = (255, 255, 255) + default_vertexcolor = (1, 1, 1) default_edgecolor = (0, 0, 0) - default_facecolor = (255, 255, 255) + default_facecolor = (1, 1, 1) def __init__(self, mesh, **kwargs): super(MeshArtist, self).__init__(**kwargs) @@ -74,10 +74,10 @@ def __init__(self, mesh, **kwargs): self._color = None self._vertex_xyz = None self._vertex_color = None - self._edge_color = None - self._face_color = None self._vertex_text = None + self._edge_color = None self._edge_text = None + self._face_color = None self._face_text = None self.mesh = mesh @@ -133,7 +133,7 @@ def color(self, color): @property def vertex_xyz(self): - if not self._vertex_xyz: + if self._vertex_xyz is None: return {vertex: self.mesh.vertex_attributes(vertex, 'xyz') for vertex in self.mesh.vertices()} return self._vertex_xyz @@ -143,7 +143,7 @@ def vertex_xyz(self, vertex_xyz): @property def vertex_color(self): - if not self._vertex_color: + if self._vertex_color is None: self._vertex_color = {vertex: self.default_vertexcolor for vertex in self.mesh.vertices()} return self._vertex_color @@ -154,35 +154,9 @@ def vertex_color(self, vertex_color): elif is_color_rgb(vertex_color): self._vertex_color = {vertex: vertex_color for vertex in self.mesh.vertices()} - @property - def edge_color(self): - if not self._edge_color: - 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 face_color(self): - if not self._face_color: - 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 vertex_text(self): - if not self._vertex_text: + if self._vertex_text is None: self._vertex_text = {vertex: str(vertex) for vertex in self.mesh.vertices()} return self._vertex_text @@ -195,9 +169,22 @@ def vertex_text(self, text): elif isinstance(text, dict): self._vertex_text = text + @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 not self._edge_text: + if self._edge_text is None: self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.mesh.edges()} return self._edge_text @@ -210,9 +197,22 @@ def edge_text(self, text): elif isinstance(text, dict): self._edge_text = text + @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 not self._face_text: + if self._face_text is None: self._face_text = {face: str(face) for face in self.mesh.faces()} return self._face_text @@ -281,3 +281,15 @@ def draw_faces(self, faces=None, color=None, text=None): 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 diff --git a/src/compas/artists/networkartist.py b/src/compas/artists/networkartist.py index 9170be9b29db..71b6ff86652d 100644 --- a/src/compas/artists/networkartist.py +++ b/src/compas/artists/networkartist.py @@ -53,7 +53,7 @@ class NetworkArtist(Artist): """ - default_nodecolor = (255, 255, 255) + default_nodecolor = (1, 1, 1) default_edgecolor = (0, 0, 0) def __init__(self, network, **kwargs): @@ -200,3 +200,11 @@ def draw_edges(self, edges=None, color=None, text=None): 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 diff --git a/src/compas/artists/shapeartist.py b/src/compas/artists/shapeartist.py index 0b3ed4d9bc4e..e2e2c433988b 100644 --- a/src/compas/artists/shapeartist.py +++ b/src/compas/artists/shapeartist.py @@ -35,7 +35,7 @@ class ShapeArtist(Artist): The default is ``16`` and the minimum ``3``. """ - default_color = (255, 255, 255) + default_color = (1, 1, 1) def __init__(self, shape, color=None, **kwargs): super(ShapeArtist, self).__init__(**kwargs) diff --git a/src/compas/artists/volmeshartist.py b/src/compas/artists/volmeshartist.py index 13eb489d1b32..0dd4d26acefe 100644 --- a/src/compas/artists/volmeshartist.py +++ b/src/compas/artists/volmeshartist.py @@ -77,10 +77,10 @@ class VolMeshArtist(Artist): """ - default_vertexcolor = (255, 255, 255) + default_vertexcolor = (1, 1, 1) default_edgecolor = (0, 0, 0) - default_facecolor = (210, 210, 210) - default_cellcolor = (255, 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) @@ -346,3 +346,19 @@ def draw_cells(self, cells=None, color=None, text=None): 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_blender/__init__.py b/src/compas_blender/__init__.py index 0f982d01bfb7..f2a051990eea 100644 --- a/src/compas_blender/__init__.py +++ b/src/compas_blender/__init__.py @@ -51,7 +51,7 @@ def redraw(self): __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 c62b27eccb44..985db28f475a 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -67,31 +67,33 @@ from .torusartist import TorusArtist -Artist.register(Box, BoxArtist) -Artist.register(Capsule, CapsuleArtist) -Artist.register(Cone, ConeArtist) -Artist.register(Cylinder, CylinderArtist) -Artist.register(Frame, FrameArtist) -Artist.register(Mesh, MeshArtist) -Artist.register(Network, NetworkArtist) -Artist.register(Polyhedron, PolyhedronArtist) -Artist.register(RobotModel, RobotModelArtist) -Artist.register(Sphere, SphereArtist) -Artist.register(Torus, TorusArtist) - - -@plugin(category='factories', pluggable_name='new_artist', requires=['bpy']) +@plugin(category='factories', pluggable_name='new_artist', tryfirst=True, requires=['bpy']) def new_artist_blender(cls, *args, **kwargs): + 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) + data = args[0] dtype = type(data) - if dtype not in Artist.ITEM_ARTIST: + if dtype not in BlenderArtist.ITEM_ARTIST: raise DataArtistNotRegistered('No Blender artist is registered for this data type: {}'.format(dtype)) + # TODO: move this to the plugin module and/or to a dedicated function - cls = Artist.ITEM_ARTIST[dtype] + + cls = BlenderArtist.ITEM_ARTIST[dtype] 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) diff --git a/src/compas_blender/artists/artist.py b/src/compas_blender/artists/artist.py index 13de7e436dbb..a8fe7a50469e 100644 --- a/src/compas_blender/artists/artist.py +++ b/src/compas_blender/artists/artist.py @@ -21,10 +21,11 @@ class BlenderArtist(Artist): def __init__(self, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): + super().__init__(**kwargs) + self._collection = None self.collection = collection - self.objects = [] @property def collection(self) -> bpy.types.Collection: @@ -39,9 +40,6 @@ def collection(self, value: Union[str, bpy.types.Collection]): else: raise Exception('Collection must be of type `str` or `bpy.types.Collection`.') - def clear(self): + def clear(self) -> None: """Delete all objects created by the artist.""" - if not self.objects: - return - compas_blender.delete_objects(self.objects) - self.objects = [] + compas_blender.delete_objects(self.collection.objects) diff --git a/src/compas_blender/artists/boxartist.py b/src/compas_blender/artists/boxartist.py index 43eb969ac9b7..3b1ddecdeaa3 100644 --- a/src/compas_blender/artists/boxartist.py +++ b/src/compas_blender/artists/boxartist.py @@ -24,6 +24,7 @@ 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): @@ -35,8 +36,5 @@ def draw(self): The objects created in Blender. """ vertices, faces = self.shape.to_vertices_and_faces() - objects = [] obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) - objects.append(obj) - self.objects += objects - return objects + return [obj] diff --git a/src/compas_blender/artists/capsuleartist.py b/src/compas_blender/artists/capsuleartist.py index e735474a1173..80dc78bd6d30 100644 --- a/src/compas_blender/artists/capsuleartist.py +++ b/src/compas_blender/artists/capsuleartist.py @@ -25,6 +25,7 @@ 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, u=None, v=None): @@ -47,8 +48,5 @@ def draw(self, u=None, v=None): u = u or self.u v = v or self.v vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) - objects = [] obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) - objects.append(obj) - self.objects += objects - return objects + return [obj] diff --git a/src/compas_blender/artists/coneartist.py b/src/compas_blender/artists/coneartist.py index 7b11323c9de1..632d2eafec32 100644 --- a/src/compas_blender/artists/coneartist.py +++ b/src/compas_blender/artists/coneartist.py @@ -25,6 +25,7 @@ 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, u=None): @@ -43,8 +44,5 @@ def draw(self, u=None): """ u = u or self.u vertices, faces = self.shape.to_vertices_and_faces(u=u) - objects = [] obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) - objects.append(obj) - self.objects += objects - return objects + return [obj] diff --git a/src/compas_blender/artists/cylinderartist.py b/src/compas_blender/artists/cylinderartist.py index ac6955f02320..d361322b99a4 100644 --- a/src/compas_blender/artists/cylinderartist.py +++ b/src/compas_blender/artists/cylinderartist.py @@ -25,6 +25,7 @@ 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, u=None, v=None): @@ -47,8 +48,5 @@ def draw(self, u=None, v=None): u = u or self.u v = v or self.v vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) - objects = [] obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) - objects.append(obj) - self.objects += objects - return objects + return [obj] diff --git a/src/compas_blender/artists/frameartist.py b/src/compas_blender/artists/frameartist.py index 0c726fc007c0..0d40ed8170e8 100644 --- a/src/compas_blender/artists/frameartist.py +++ b/src/compas_blender/artists/frameartist.py @@ -47,7 +47,9 @@ def __init__(self, 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) @@ -78,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. @@ -98,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 194695633db3..d7cf3ff41f67 100644 --- a/src/compas_blender/artists/meshartist.py +++ b/src/compas_blender/artists/meshartist.py @@ -32,6 +32,25 @@ class MeshArtist(BlenderArtist, MeshArtist): 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 ---------- @@ -53,8 +72,20 @@ class MeshArtist(BlenderArtist, MeshArtist): 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._vertexcollection = None self._edgecollection = None self._facecollection = None @@ -64,6 +95,17 @@ def __init__(self, self._edgelabelcollection = None self._facelabelcollection = None + 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: if not self._vertexcollection: @@ -112,11 +154,31 @@ def facelabelcollection(self) -> bpy.types.Collection: self._facelabelcollection = compas_blender.create_collection('FaceLabels', parent=self.collection) return self._facelabelcollection + # ========================================================================== + # clear + # ========================================================================== + + 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) + # ========================================================================== # 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 @@ -126,20 +188,25 @@ def draw(self) -> None: """ self.clear() - self.draw_vertices() - self.draw_faces() - self.draw_edges() + 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) def draw_mesh(self) -> List[bpy.types.Object]: """Draw the mesh.""" vertices, faces = self.mesh.to_vertices_and_faces() obj = compas_blender.draw_mesh(vertices, faces, name=self.mesh.name, collection=self.collection) - self.objects += [obj] return [obj] 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 @@ -153,56 +220,23 @@ def draw_vertices(self, Returns ------- list of :class:`bpy.types.Object` - """ self.vertex_color = color - vertices = vertices or list(self.mesh.vertices()) + 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': self.vertex_color.get(vertex, self.default_vertexcolor), 'radius': 0.01 }) - objects = compas_blender.draw_points(points, self.vertexcollection) - self.objects += objects - 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` - - """ - self.face_color = color - faces = faces or list(self.mesh.faces()) - facets = [] - for face in faces: - facets.append({ - 'points': self.mesh.face_coordinates(face), - 'name': f"{self.mesh.name}.face.{face}", - 'color': self.face_color.get(face, self.default_facecolor) - }) - objects = compas_blender.draw_faces(facets, self.facecollection) - self.objects += objects - 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 @@ -219,18 +253,45 @@ def draw_edges(self, """ self.edge_color = color - edges = edges or list(self.mesh.edges()) + 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]), + '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.objects += objects - 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 @@ -258,11 +319,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({ @@ -271,9 +332,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.objects += objects - return objects + return compas_blender.draw_lines(lines, collection=self.vertexnormalcollection) def draw_facenormals(self, faces: Optional[List[List[int]]] = None, @@ -297,13 +356,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({ @@ -312,9 +369,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.objects += objects - return objects + return compas_blender.draw_lines(lines, collection=self.facenormalcollection) # ========================================================================== # draw labels @@ -339,24 +394,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.objects += objects - 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, @@ -377,23 +431,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.objects += objects - 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, @@ -414,22 +465,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.objects += objects - 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 7d2f4f692b52..9a5f283eb49a 100644 --- a/src/compas_blender/artists/networkartist.py +++ b/src/compas_blender/artists/networkartist.py @@ -28,6 +28,18 @@ class NetworkArtist(BlenderArtist, NetworkArtist): A COMPAS network. 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 ---------- @@ -39,19 +51,33 @@ class NetworkArtist(BlenderArtist, NetworkArtist): The collection containing the node labels. edgelabelcollection : :class:`bpy.types.Collection` The collection containing the edge labels. - """ 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._nodecollection = None self._edgecollection = None self._nodelabelcollection = None self._edgelabelcollection = None + 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: if not self._nodecollection: @@ -76,22 +102,37 @@ def edgelabelcollection(self) -> bpy.types.Collection: self._edgelabelcollection = compas_blender.create_collection('EdgeLabels', parent=self.collection) return self._edgelabelcollection - 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() - self.draw_nodes() - self.draw_edges() + if self.show_nodes: + 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) -> 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 @@ -108,21 +149,21 @@ def draw_nodes(self, """ self.node_color = color - nodes = nodes or list(self.network.nodes()) + 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': self.node_color.get(node, self.default_nodecolor), - 'radius': 0.05}) - objects = compas_blender.draw_points(points, self.nodecollection) - self.objects += objects - return objects + '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 @@ -139,18 +180,17 @@ def draw_edges(self, """ self.edge_color = color - edges = edges or list(self.network.edges()) + 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]), + '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.objects += objects - return objects + 'width': 0.02 + }) + return compas_blender.draw_lines(lines, self.edgecollection) def draw_nodelabels(self, text: Optional[Dict[int, str]] = None, @@ -172,24 +212,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.objects += objects - 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, @@ -211,20 +250,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.objects += objects - 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 index a3d61e05901e..a41360202fc2 100644 --- a/src/compas_blender/artists/polyhedronartist.py +++ b/src/compas_blender/artists/polyhedronartist.py @@ -24,6 +24,7 @@ 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): @@ -35,8 +36,5 @@ def draw(self): The objects created in Blender. """ vertices, faces = self.shape.to_vertices_and_faces() - objects = [] obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) - objects.append(obj) - self.objects += objects - return objects + return [obj] diff --git a/src/compas_blender/artists/sphereartist.py b/src/compas_blender/artists/sphereartist.py index d2bebb4c965f..3d14b7a93f2f 100644 --- a/src/compas_blender/artists/sphereartist.py +++ b/src/compas_blender/artists/sphereartist.py @@ -25,6 +25,7 @@ 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, u=None, v=None): @@ -47,8 +48,5 @@ def draw(self, u=None, v=None): u = u or self.u v = v or self.v vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) - objects = [] obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) - objects.append(obj) - self.objects += objects - return objects + return [obj] diff --git a/src/compas_blender/artists/torusartist.py b/src/compas_blender/artists/torusartist.py index 611c31af27ee..c04553dc875a 100644 --- a/src/compas_blender/artists/torusartist.py +++ b/src/compas_blender/artists/torusartist.py @@ -25,6 +25,7 @@ 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, u=None, v=None): @@ -47,8 +48,5 @@ def draw(self, u=None, v=None): u = u or self.u v = v or self.v vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) - objects = [] obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) - objects.append(obj) - self.objects += objects - return objects + return [obj] diff --git a/src/compas_blender/artists/volmeshartist.py b/src/compas_blender/artists/volmeshartist.py index ba74bd4aece7..d1170784446a 100644 --- a/src/compas_blender/artists/volmeshartist.py +++ b/src/compas_blender/artists/volmeshartist.py @@ -13,4 +13,5 @@ class VolMeshArtist(BlenderArtist, MeshArtist): def __init__(self, volmesh, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): + super().__init__(volmesh=volmesh, collection=collection or volmesh.name, **kwargs) diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index 68986e609c2e..735740350cc6 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -58,6 +58,7 @@ 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 @@ -81,28 +82,47 @@ from .volmeshartist import VolMeshArtist from .robotmodelartist import RobotModelArtist -Artist.register(Circle, CircleArtist) -Artist.register(Frame, FrameArtist) -Artist.register(Line, LineArtist) -Artist.register(Point, PointArtist) -Artist.register(Polyline, PolylineArtist) -Artist.register(Mesh, MeshArtist) -Artist.register(Network, NetworkArtist) -Artist.register(VolMesh, VolMeshArtist) +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) @plugin(category='factories', pluggable_name='new_artist', requires=['ghpythonlib']) def new_artist_gh(cls, *args, **kwargs): + 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) + data = args[0] dtype = type(data) - if dtype not in Artist.ITEM_ARTIST: + if dtype not in GHArtist.ITEM_ARTIST: raise DataArtistNotRegistered('No GH artist is registered for this data type: {}'.format(dtype)) + # TODO: move this to the plugin module and/or to a dedicated function - cls = Artist.ITEM_ARTIST[dtype] + + cls = GHArtist.ITEM_ARTIST[dtype] for name, value in inspect.getmembers(cls): if inspect.ismethod(value): if hasattr(value, '__isabstractmethod__'): raise Exception('Abstract method not implemented') + return super(Artist, cls).__new__(cls) diff --git a/src/compas_ghpython/artists/circleartist.py b/src/compas_ghpython/artists/circleartist.py index 23c1509d16b1..b45477b54d3b 100644 --- a/src/compas_ghpython/artists/circleartist.py +++ b/src/compas_ghpython/artists/circleartist.py @@ -25,7 +25,6 @@ def draw(self): Returns ------- :class:`Rhino.Geometry.Circle` - """ circles = [self._get_args(self.primitive, self.color)] return compas_ghpython.draw_circles(circles)[0] diff --git a/src/compas_ghpython/artists/frameartist.py b/src/compas_ghpython/artists/frameartist.py index ad10adf66b68..3df4bb69a839 100644 --- a/src/compas_ghpython/artists/frameartist.py +++ b/src/compas_ghpython/artists/frameartist.py @@ -31,7 +31,6 @@ class FrameArtist(GHArtist, PrimitiveArtist): 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, **kwargs): @@ -48,7 +47,6 @@ def draw(self): Returns ------- :class:`Rhino.Geometry.Plane` - """ return compas_ghpython.draw_frame(self.primitive) @@ -58,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] @@ -69,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 8eecdd935edf..3579ed54a1fd 100644 --- a/src/compas_ghpython/artists/lineartist.py +++ b/src/compas_ghpython/artists/lineartist.py @@ -14,7 +14,6 @@ class LineArtist(GHArtist, PrimitiveArtist): ---------- line : :class:`compas.geometry.Line` A COMPAS line. - """ def __init__(self, line, **kwargs): @@ -26,7 +25,6 @@ def draw(self): 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/networkartist.py b/src/compas_ghpython/artists/networkartist.py index 755106bba424..9d9a4979dd94 100644 --- a/src/compas_ghpython/artists/networkartist.py +++ b/src/compas_ghpython/artists/networkartist.py @@ -50,7 +50,6 @@ def draw_nodes(self, nodes=None, color=None): Returns ------- list of :class:`Rhino.Geometry.Point3d` - """ self.node_color = color node_xyz = self.node_xyz @@ -79,7 +78,6 @@ def draw_edges(self, edges=None, color=None): Returns ------- list of :class:`Rhino.Geometry.Line` - """ self.edge_color = color node_xyz = self.node_xyz diff --git a/src/compas_ghpython/artists/pointartist.py b/src/compas_ghpython/artists/pointartist.py index 975c99ba8be0..68408fe7a5c1 100644 --- a/src/compas_ghpython/artists/pointartist.py +++ b/src/compas_ghpython/artists/pointartist.py @@ -25,7 +25,6 @@ def draw(self): 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/volmeshartist.py b/src/compas_ghpython/artists/volmeshartist.py index 057fe7dca598..d2587f20be1a 100644 --- a/src/compas_ghpython/artists/volmeshartist.py +++ b/src/compas_ghpython/artists/volmeshartist.py @@ -46,7 +46,6 @@ def draw_vertices(self, vertices=None, color=None): Returns ------- list of :class:`Rhino.Geometry.Point3d` - """ self.vertex_color = color vertices = vertices or list(self.volmesh.vertices()) @@ -75,7 +74,6 @@ def draw_edges(self, edges=None, color=None): Returns ------- list of :class:`Rhino.Geometry.Line` - """ self.edge_color = color edges = edges or list(self.volmesh.edges()) @@ -105,7 +103,6 @@ def draw_faces(self, faces=None, color=None, join_faces=False): Returns ------- list of :class:`Rhino.Geometry.Mesh` - """ self.face_color = color faces = faces or list(self.volmesh.faces()) diff --git a/src/compas_plotters/artists/__init__.py b/src/compas_plotters/artists/__init__.py index a265bc645071..a2fd4ab0b9c2 100644 --- a/src/compas_plotters/artists/__init__.py +++ b/src/compas_plotters/artists/__init__.py @@ -57,29 +57,32 @@ 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) - @plugin(category='factories', pluggable_name='new_artist', trylast=True, requires=['matplotlib']) def new_artist_plotter(cls, *args, **kwargs): + 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) + data = args[0] dtype = type(data) - if dtype not in Artist.ITEM_ARTIST: + if dtype not in PlotterArtist.ITEM_ARTIST: raise DataArtistNotRegistered('No Plotter artist is registered for this data type: {}'.format(dtype)) + # TODO: move this to the plugin module and/or to a dedicated function - cls = Artist.ITEM_ARTIST[dtype] + + cls = PlotterArtist.ITEM_ARTIST[dtype] 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) diff --git a/src/compas_plotters/artists/meshartist.py b/src/compas_plotters/artists/meshartist.py index 7c26377e6de3..7e22d76158ed 100644 --- a/src/compas_plotters/artists/meshartist.py +++ b/src/compas_plotters/artists/meshartist.py @@ -16,11 +16,50 @@ class MeshArtist(PlotterArtist, MeshArtist): - """Artist for COMPAS mesh data structures.""" + """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 + + 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. + + Class Attributes + ---------------- + default_vertexsize : int + default_edgewidth : float + zorder_vertices : int + zorder_edges : int + zorder_faces : int + """ default_vertexsize: int = 5 default_edgewidth: float = 1.0 @@ -31,41 +70,52 @@ class MeshArtist(PlotterArtist, MeshArtist): def __init__(self, mesh: Mesh, - show_vertices: bool = True, - show_edges: bool = True, - show_faces: bool = True, vertices: Optional[List[int]] = None, edges: Optional[List[int]] = None, faces: Optional[List[int]] = None, - 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), + edgewidth: float = 1.0, + show_vertices: bool = True, + show_edges: bool = True, + show_faces: bool = True, + vertexsize: int = 5, + sizepolicy: Literal['relative', 'absolute'] = 'relative', **kwargs: Any): super().__init__(mesh=mesh, **kwargs) - self._mpl_vertex_collection = None - self._mpl_edge_collection = None - self._mpl_face_collection = None self._edge_width = 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.vertices = vertices self.edges = edges self.faces = faces + + self.vertex_color = vertexcolor + self.edge_color = edgecolor + self.face_color = facecolor + self.edge_width = edgewidth + self.show_vertices = show_vertices self.show_edges = show_edges self.show_faces = show_faces + self.vertexsize = vertexsize self.sizepolicy = sizepolicy - self.edgewidth = edgewidth - self.vertex_color = vertexcolor - self.edge_color = edgecolor - self.face_color = facecolor @property def item(self): + """Mesh: Alias for ``~MeshArtist.mesh``""" return self.mesh @item.setter @@ -90,24 +140,85 @@ def edge_width(self, edgewidth: Union[float, Dict[Tuple[int, int], float]]): def data(self) -> List[List[float]]: return self.mesh.vertices_attributes('xy') + def clear(self): + self.clear_vertices() + self.clear_edges() + self.clear_faces() + + def clear_vertices(self) -> None: + if self._vertexcollection: + self.plotter.axes.remove_collection(self._vertexcollection) + self._vertexcollection = None + + 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=None, - edges=None, - faces=None, - vertexcolor=None, - edgecolor=None, - facecolor=None) -> None: - """Draw the mesh.""" - if self.show_faces: - self.draw_faces(faces=faces, color=facecolor) + 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: + 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) - if self.show_vertices: - self.draw_vertices(vertices=vertices, color=vertexcolor) + def redraw(self) -> None: + raise NotImplementedError + + 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() - def draw_vertices(self, vertices=None, color=None, text=None): if vertices: self.vertices = vertices if color: @@ -137,9 +248,28 @@ def draw_vertices(self, vertices=None, color=None, text=None): alpha=1.0 ) self.plotter.axes.add_collection(collection) - self._mpl_vertex_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() - def draw_edges(self, edges=None, color=None, text=None): if edges: self.edges = edges if color: @@ -162,9 +292,28 @@ def draw_edges(self, edges=None, color=None, text=None): zorder=self.zorder_edges ) self.plotter.axes.add_collection(collection) - self._mpl_edge_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() - def draw_faces(self, faces=None, color=None, text=None): if faces: self.faces = faces if color: @@ -191,7 +340,4 @@ def draw_faces(self, faces=None, color=None, text=None): zorder=self.zorder_faces ) self.plotter.axes.add_collection(collection) - self._mpl_face_collection = collection - - def redraw(self) -> None: - raise NotImplementedError + self._facecollection = collection diff --git a/src/compas_plotters/artists/networkartist.py b/src/compas_plotters/artists/networkartist.py index 32917268ec21..e9c7b51d51e0 100644 --- a/src/compas_plotters/artists/networkartist.py +++ b/src/compas_plotters/artists/networkartist.py @@ -1,7 +1,13 @@ -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 .artist import PlotterArtist @@ -9,10 +15,43 @@ class NetworkArtist(PlotterArtist): - """Artist for COMPAS network data structures.""" - - default_nodecolor: Color = (1, 1, 1) - default_edgecolor: Color = (0, 0, 0) + """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 + """ default_nodesize: int = 5 default_edgewidth: float = 1.0 @@ -22,122 +61,185 @@ class NetworkArtist(PlotterArtist): 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)): + **kwargs): + + super().__init__(network=network, **kwargs) - super().__init__(network) + self._nodecollection = None + self._edgecollection = None + self._edge_width = None - self._mpl_node_collection = None - self._mpl_edge_collection = None - self._nodecolor = None - self._edgecolor = None - self._edgewidth = None - self.network = network + self.nodes = nodes + self.edges = edges + self.node_color = nodecolor + 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 @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 item(self): + """Network: Alias for ``~NetworkArtist.network``""" + return self.network - @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 = {} + @item.setter + def item(self, item: Network): + self.network = item @property - def edgewidth(self) -> Dict[Tuple[int, int], float]: + def edge_width(self) -> Dict[Tuple[int, int], float]: """dict: Edge widths.""" - return self._edgewidth + return self._edge_width - @edgewidth.setter - def edgewidth(self, edgewidth: Union[float, Dict[Tuple[int, int], float]]): + @edge_width.setter + def edge_width(self, edgewidth: Union[float, Dict[Tuple[int, int], float]]): if isinstance(edgewidth, dict): - self._edgewidth = edgewidth + self._edge_width = edgewidth elif isinstance(edgewidth, (int, float)): - self._edgewidth = {edge: edgewidth for edge in self.network.edges()} + self._edge_width = {edge: edgewidth for edge in self.network.edges()} else: - self._edgewidth = {} + self._edge_width = {} @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()} - - 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.plotter.axes.add_collection(collection) - self._mpl_edge_collection = collection - + def clear(self): + self.clear_nodes() + self.clear_edges() + + 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: - 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) + self.draw_nodes(nodes=nodes, color=nodecolor) + if self.show_edges: + self.draw_edges(edges=edges, color=edgecolor) def redraw(self) -> None: raise NotImplementedError + + 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 + + if self.sizepolicy == 'absolute': + size = self.nodesize / self.plotter.dpi + else: + size = self.nodesize / self.network.number_of_nodes() + + circles = [] + for node in self.nodes: + x, y = self.node_xyz[node][:2] + circle = Circle( + [x, y], + radius=size, + facecolor=self.node_color.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) + 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/plotter.py b/src/compas_plotters/plotter.py index 6365433fcd0f..eb0cbb96d653 100644 --- a/src/compas_plotters/plotter.py +++ b/src/compas_plotters/plotter.py @@ -6,7 +6,7 @@ from PIL import Image import compas -from .artists import Artist +from .artists import PlotterArtist class Plotter: @@ -182,12 +182,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 # ========================================================================= @@ -245,12 +245,12 @@ 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 = PlotterArtist.build(item, **kwargs) artist.plotter = self artist.draw() self._artists.append(artist) @@ -265,16 +265,16 @@ 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 = PlotterArtist.build_as(item, artist_type, **kwargs) artist.plotter = self 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: @@ -290,7 +290,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/artists/__init__.py b/src/compas_rhino/artists/__init__.py index edb3f80b08e7..020ee18a2b8c 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -77,6 +77,7 @@ 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 @@ -123,39 +124,58 @@ from .volmeshartist import VolMeshArtist from .robotmodelartist import RobotModelArtist -Artist.register(Circle, CircleArtist) -Artist.register(Frame, FrameArtist) -Artist.register(Line, LineArtist) -Artist.register(Plane, PlaneArtist) -Artist.register(Point, PointArtist) -Artist.register(Polygon, PolygonArtist) -Artist.register(Polyline, PolylineArtist) -Artist.register(Vector, VectorArtist) -Artist.register(Box, BoxArtist) -Artist.register(Capsule, CapsuleArtist) -Artist.register(Cone, ConeArtist) -Artist.register(Cylinder, CylinderArtist) -Artist.register(Polyhedron, PolyhedronArtist) -Artist.register(Sphere, SphereArtist) -Artist.register(Torus, TorusArtist) -Artist.register(Mesh, MeshArtist) -Artist.register(Network, NetworkArtist) -Artist.register(VolMesh, VolMeshArtist) -Artist.register(RobotModel, 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) @plugin(category='factories', pluggable_name='new_artist', requires=['Rhino']) def new_artist_rhino(cls, *args, **kwargs): + 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) + data = args[0] dtype = type(data) - if dtype not in Artist.ITEM_ARTIST: + if dtype not in RhinoArtist.ITEM_ARTIST: raise DataArtistNotRegistered('No Rhino artist is registered for this data type: {}'.format(dtype)) + # TODO: move this to the plugin module and/or to a dedicated function - cls = Artist.ITEM_ARTIST[dtype] + + cls = RhinoArtist.ITEM_ARTIST[dtype] for name, value in inspect.getmembers(cls): if inspect.ismethod(value): if hasattr(value, '__isabstractmethod__'): raise Exception('Abstract method not implemented') + return super(Artist, cls).__new__(cls) diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index ffe365d472af..ccff9fdc5867 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -29,14 +29,80 @@ class MeshArtist(RhinoArtist, MeshArtist): The name of the layer that will contain the mesh. """ - def __init__(self, mesh, layer=None, **kwargs): + 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) - def clear_by_name(self): - """Clear all objects in the "namespace" of the associated mesh.""" + 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(self): guids = compas_rhino.get_objects(name="{}.*".format(self.mesh.name)) compas_rhino.delete_objects(guids, purge=True) + 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 # ========================================================================== @@ -70,14 +136,18 @@ def draw(self, vertices=None, edges=None, faces=None, vertexcolor=None, edgecolo Returns ------- - list - The GUIDs of the created Rhino objects. + None """ - guids = self.draw_vertices(vertices=vertices, color=vertexcolor) - guids += self.draw_edges(edges=edges, color=edgecolor) - guids += self.draw_faces(faces=faces, color=facecolor, join_faces=join_faces) - return guids + 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. @@ -119,7 +189,7 @@ def draw_mesh(self, color=None, disjoint=False): for a, b in pairwise(face + face[0:1]): new_faces.append([centroid, a, b, b]) 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] @@ -151,7 +221,40 @@ def draw_vertices(self, vertices=None, color=None): 'name': "{}.vertex.{}".format(self.mesh.name, vertex), 'color': self.vertex_color.get(vertex, self.default_vertexcolor) }) - return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) + 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. @@ -185,44 +288,13 @@ def draw_faces(self, faces=None, color=None, join_faces=False): '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)) - 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 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) - }) - return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) + 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) + guids = [guid] + return guids # ========================================================================== # draw normals @@ -250,7 +322,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] @@ -287,7 +359,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)]) @@ -325,35 +397,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 ------- @@ -361,36 +434,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 ------- @@ -398,19 +470,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 e30a44790579..8714cfb5d5d5 100644 --- a/src/compas_rhino/artists/networkartist.py +++ b/src/compas_rhino/artists/networkartist.py @@ -23,16 +23,64 @@ class NetworkArtist(RhinoArtist, NetworkArtist): 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 """ - def __init__(self, network, layer=None, **kwargs): + 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) - def clear_by_name(self): - """Clear all objects in the "namespace" of the associated network.""" + 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(self): guids = compas_rhino.get_objects(name="{}.*".format(self.network.name)) compas_rhino.delete_objects(guids, purge=True) + 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 # ========================================================================== @@ -59,8 +107,8 @@ def draw(self, nodes=None, edges=None, nodecolor=None, edgecolor=None): ------- list The GUIDs of the created Rhino objects. - """ + self.clear() guids = self.draw_nodes(nodes=nodes, color=nodecolor) guids += self.draw_edges(edges=edges, color=edgecolor) return guids @@ -81,11 +129,10 @@ def draw_nodes(self, nodes=None, color=None): ------- list The GUIDs of the created Rhino objects. - """ self.node_color = color node_xyz = self.node_xyz - nodes = nodes or list(self.network.nodes()) + nodes = nodes or self.nodes points = [] for node in nodes: points.append({ @@ -111,11 +158,10 @@ def draw_edges(self, edges=None, color=None): ------- list The GUIDs of the created Rhino objects. - """ self.edge_color = color node_xyz = self.node_xyz - edges = edges or list(self.network.edges()) + edges = edges or self.edges lines = [] for edge in edges: lines.append({ @@ -146,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): @@ -183,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) From f14e4f4c0a154f2f8d0da58deb0ead4712ccc344 Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 28 Sep 2021 15:16:16 +0200 Subject: [PATCH 47/71] replace build and build_as --- src/compas/artists/artist.py | 25 ------------------------- src/compas/artists/meshartist.py | 8 ++++---- src/compas_blender/artists/__init__.py | 14 ++++++++++---- src/compas_ghpython/artists/__init__.py | 14 ++++++++++---- src/compas_plotters/artists/__init__.py | 14 ++++++++++---- src/compas_plotters/plotter.py | 4 ++-- src/compas_rhino/artists/__init__.py | 14 ++++++++++---- 7 files changed, 46 insertions(+), 47 deletions(-) diff --git a/src/compas/artists/artist.py b/src/compas/artists/artist.py index 2371e0ad1343..3464ae524d9c 100644 --- a/src/compas/artists/artist.py +++ b/src/compas/artists/artist.py @@ -24,31 +24,6 @@ def __new__(cls, *args, **kwargs): def register(item_type, artist_type): Artist.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.artists.Artist` - An artist of the type matching the provided item according to the item-artist map ``~Artist.ITEM_ARTIST``. - The map is created by registering item-artist type pairs using ``~Artist.register``. - """ - artist_type = Artist.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 - @abstractmethod def draw(self): raise NotImplementedError diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index a7ee56747fad..0b579a86884a 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -60,10 +60,10 @@ class MeshArtist(Artist): Mapping between faces and text labels. """ - default_color = (0, 0, 0) - default_vertexcolor = (1, 1, 1) - default_edgecolor = (0, 0, 0) - default_facecolor = (1, 1, 1) + 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) def __init__(self, mesh, **kwargs): super(MeshArtist, self).__init__(**kwargs) diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 985db28f475a..b687a287b942 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -69,6 +69,8 @@ @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 + BlenderArtist.register(Box, BoxArtist) BlenderArtist.register(Capsule, CapsuleArtist) BlenderArtist.register(Cone, ConeArtist) @@ -82,13 +84,17 @@ def new_artist_blender(cls, *args, **kwargs): BlenderArtist.register(Torus, TorusArtist) data = args[0] - dtype = type(data) - if dtype not in BlenderArtist.ITEM_ARTIST: - raise DataArtistNotRegistered('No Blender artist is registered for this data type: {}'.format(dtype)) + + 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 - cls = BlenderArtist.ITEM_ARTIST[dtype] for name, value in inspect.getmembers(cls): if inspect.isfunction(value): if hasattr(value, '__isabstractmethod__'): diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index 735740350cc6..4a0a64c78364 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -101,6 +101,8 @@ @plugin(category='factories', pluggable_name='new_artist', requires=['ghpythonlib']) def new_artist_gh(cls, *args, **kwargs): + # "lazy registration" seems necessary to avoid item-artist pairs to be overwritten unintentionally + GHArtist.register(Circle, CircleArtist) GHArtist.register(Frame, FrameArtist) GHArtist.register(Line, LineArtist) @@ -111,13 +113,17 @@ def new_artist_gh(cls, *args, **kwargs): GHArtist.register(VolMesh, VolMeshArtist) data = args[0] - dtype = type(data) - if dtype not in GHArtist.ITEM_ARTIST: - raise DataArtistNotRegistered('No GH artist is registered for this data type: {}'.format(dtype)) + + 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 - cls = GHArtist.ITEM_ARTIST[dtype] for name, value in inspect.getmembers(cls): if inspect.ismethod(value): if hasattr(value, '__isabstractmethod__'): diff --git a/src/compas_plotters/artists/__init__.py b/src/compas_plotters/artists/__init__.py index a2fd4ab0b9c2..7dbf589b4b90 100644 --- a/src/compas_plotters/artists/__init__.py +++ b/src/compas_plotters/artists/__init__.py @@ -60,6 +60,8 @@ @plugin(category='factories', pluggable_name='new_artist', trylast=True, requires=['matplotlib']) def new_artist_plotter(cls, *args, **kwargs): + # "lazy registration" seems necessary to avoid item-artist pairs to be overwritten unintentionally + PlotterArtist.register(Point, PointArtist) PlotterArtist.register(Vector, VectorArtist) PlotterArtist.register(Line, LineArtist) @@ -71,13 +73,17 @@ def new_artist_plotter(cls, *args, **kwargs): PlotterArtist.register(Network, NetworkArtist) data = args[0] - dtype = type(data) - if dtype not in PlotterArtist.ITEM_ARTIST: - raise DataArtistNotRegistered('No Plotter artist is registered for this data type: {}'.format(dtype)) + + 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 - cls = PlotterArtist.ITEM_ARTIST[dtype] for name, value in inspect.getmembers(cls): if inspect.isfunction(value): if hasattr(value, '__isabstractmethod__'): diff --git a/src/compas_plotters/plotter.py b/src/compas_plotters/plotter.py index eb0cbb96d653..e7daf75320c2 100644 --- a/src/compas_plotters/plotter.py +++ b/src/compas_plotters/plotter.py @@ -250,7 +250,7 @@ def add(self, """Add a COMPAS geometry object or data structure to the plot. """ if not artist: - artist = PlotterArtist.build(item, **kwargs) + artist = PlotterArtist(item, **kwargs) artist.plotter = self artist.draw() self._artists.append(artist) @@ -268,7 +268,7 @@ def add_as(self, artist_type: PlotterArtist, **kwargs) -> PlotterArtist: """Add a COMPAS geometry object or data structure using a specific artist type.""" - artist = PlotterArtist.build_as(item, artist_type, **kwargs) + artist = PlotterArtist(item, artist_type=artist_type, **kwargs) artist.plotter = self artist.draw() self._artists.append(artist) diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 020ee18a2b8c..2a0b2e1d1ae4 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -143,6 +143,8 @@ @plugin(category='factories', pluggable_name='new_artist', requires=['Rhino']) def new_artist_rhino(cls, *args, **kwargs): + # "lazy registration" seems necessary to avoid item-artist pairs to be overwritten unintentionally + RhinoArtist.register(Circle, CircleArtist) RhinoArtist.register(Frame, FrameArtist) RhinoArtist.register(Line, LineArtist) @@ -164,13 +166,17 @@ def new_artist_rhino(cls, *args, **kwargs): RhinoArtist.register(RobotModel, RobotModelArtist) data = args[0] - dtype = type(data) - if dtype not in RhinoArtist.ITEM_ARTIST: - raise DataArtistNotRegistered('No Rhino artist is registered for this data type: {}'.format(dtype)) + + 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] # TODO: move this to the plugin module and/or to a dedicated function - cls = RhinoArtist.ITEM_ARTIST[dtype] for name, value in inspect.getmembers(cls): if inspect.ismethod(value): if hasattr(value, '__isabstractmethod__'): From 29134a36b3b1d5db12fe4891d1f15191f7f1fa31 Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 28 Sep 2021 18:02:05 +0200 Subject: [PATCH 48/71] make collection paths unique to avoid all sorts of mayhem --- src/compas_blender/utilities/collections.py | 29 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) 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 From 3bb98a4fd47c68f280af4cb3acfcd70147e0ec0a Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 28 Sep 2021 18:02:19 +0200 Subject: [PATCH 49/71] plotter singleton --- src/compas_plotters/plotter.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/compas_plotters/plotter.py b/src/compas_plotters/plotter.py index e7daf75320c2..118f9f610769 100644 --- a/src/compas_plotters/plotter.py +++ b/src/compas_plotters/plotter.py @@ -9,7 +9,16 @@ 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 @@ -211,6 +220,7 @@ def zoom_extents(self, padding: Optional[int] = None) -> None: width, height = self.figsize fig_aspect = width / height data = [] + print(self.artists) for artist in self.artists: data += artist.data x, y = zip(* data) @@ -251,7 +261,6 @@ def add(self, """ if not artist: artist = PlotterArtist(item, **kwargs) - artist.plotter = self artist.draw() self._artists.append(artist) return artist @@ -269,7 +278,6 @@ def add_as(self, **kwargs) -> PlotterArtist: """Add a COMPAS geometry object or data structure using a specific artist type.""" artist = PlotterArtist(item, artist_type=artist_type, **kwargs) - artist.plotter = self artist.draw() self._artists.append(artist) return artist From dfbd4d6b8d0a53a704d89152030f13deaacb6e61 Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 28 Sep 2021 18:02:33 +0200 Subject: [PATCH 50/71] don't pull up artists --- src/compas_plotters/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compas_plotters/__init__.py b/src/compas_plotters/__init__.py index 05edd23b9ff7..5d6930a015d2 100644 --- a/src/compas_plotters/__init__.py +++ b/src/compas_plotters/__init__.py @@ -21,7 +21,7 @@ __version__ = '1.8.1' from .core import * # noqa: F401 F403 -from .artists import * # noqa: F401 F403 +# from .artists import * # noqa: F401 F403 from .plotter import Plotter From 386b017e2859e879df0adad9bc31d227b3ad30c8 Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 28 Sep 2021 18:02:54 +0200 Subject: [PATCH 51/71] refer to plotter singleton for parenting --- src/compas_plotters/artists/artist.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/compas_plotters/artists/artist.py b/src/compas_plotters/artists/artist.py index 1dd4e6b4ce7c..f38a49e75d1b 100644 --- a/src/compas_plotters/artists/artist.py +++ b/src/compas_plotters/artists/artist.py @@ -8,7 +8,14 @@ class PlotterArtist(Artist): def __init__(self, **kwargs): super().__init__(**kwargs) - self.plotter = None + self._plotter = None + + @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() From 3a173618cd97b316a010ae36763365ad57b1b10e Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 28 Sep 2021 18:03:30 +0200 Subject: [PATCH 52/71] move collections, vertex size, node size and edge width to base artist --- src/compas/artists/meshartist.py | 49 +++++++++++++ src/compas/artists/networkartist.py | 42 +++++++++++- src/compas_blender/artists/meshartist.py | 9 --- src/compas_blender/artists/networkartist.py | 25 +++++-- src/compas_plotters/artists/meshartist.py | 72 ++++++-------------- src/compas_plotters/artists/networkartist.py | 41 ++--------- src/compas_rhino/artists/meshartist.py | 2 - 7 files changed, 137 insertions(+), 103 deletions(-) diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index 0b579a86884a..515b0002c74a 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -26,6 +26,8 @@ class MeshArtist(Artist): 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 ---------- @@ -65,8 +67,12 @@ class MeshArtist(Artist): 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 @@ -75,10 +81,22 @@ def __init__(self, mesh, **kwargs): 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 @@ -169,6 +187,19 @@ def vertex_text(self, text): 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: @@ -197,6 +228,19 @@ def edge_text(self, text): 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: @@ -293,3 +337,8 @@ def clear_edges(self): @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 index 71b6ff86652d..3ed343e9d055 100644 --- a/src/compas/artists/networkartist.py +++ b/src/compas/artists/networkartist.py @@ -56,8 +56,12 @@ class NetworkArtist(Artist): 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 @@ -66,6 +70,12 @@ def __init__(self, network, **kwargs): 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 @@ -120,6 +130,19 @@ def node_color(self, 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: @@ -163,6 +186,19 @@ def edge_text(self, text): 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. @@ -184,7 +220,7 @@ def draw_nodes(self, nodes=None, color=None, text=None): @abstractmethod def draw_edges(self, edges=None, color=None, text=None): - """Draw the edges of the mesh. + """Draw the edges of the network. Parameters ---------- @@ -208,3 +244,7 @@ def clear_nodes(self): @abstractmethod def clear_edges(self): raise NotImplementedError + + def clear(self): + self.clear_nodes() + self.clear_edges() diff --git a/src/compas_blender/artists/meshartist.py b/src/compas_blender/artists/meshartist.py index d7cf3ff41f67..8acf734c0f05 100644 --- a/src/compas_blender/artists/meshartist.py +++ b/src/compas_blender/artists/meshartist.py @@ -86,15 +86,6 @@ def __init__(self, super().__init__(mesh=mesh, collection=collection or mesh.name, **kwargs) - 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.vertices = vertices self.edges = edges self.faces = faces diff --git a/src/compas_blender/artists/networkartist.py b/src/compas_blender/artists/networkartist.py index 9a5f283eb49a..8c96cbd02dfe 100644 --- a/src/compas_blender/artists/networkartist.py +++ b/src/compas_blender/artists/networkartist.py @@ -66,11 +66,6 @@ def __init__(self, super().__init__(network=network, collection=collection or network.name, **kwargs) - self._nodecollection = None - self._edgecollection = None - self._nodelabelcollection = None - self._edgelabelcollection = None - self.nodes = nodes self.edges = edges self.node_color = nodecolor @@ -102,6 +97,26 @@ def edgelabelcollection(self) -> bpy.types.Collection: self._edgelabelcollection = compas_blender.create_collection('EdgeLabels', parent=self.collection) return self._edgelabelcollection + # ========================================================================== + # clear + # ========================================================================== + + def clear_nodes(self): + compas_blender.delete_objects(self.nodecollection.objects) + + def clear_edges(self): + compas_blender.delete_objects(self.edgecollection.objects) + + def clear_nodelabels(self): + compas_blender.delete_objects(self.nodelabelcollection.objects) + + def clear_edgelabels(self): + compas_blender.delete_objects(self.edgelabelcollection.objects) + + # ========================================================================== + # draw + # ========================================================================== + def draw(self, nodes: Optional[List[int]] = None, edges: Optional[Tuple[int, int]] = None, diff --git a/src/compas_plotters/artists/meshartist.py b/src/compas_plotters/artists/meshartist.py index 7e22d76158ed..bc7605fb7aad 100644 --- a/src/compas_plotters/artists/meshartist.py +++ b/src/compas_plotters/artists/meshartist.py @@ -54,16 +54,11 @@ class MeshArtist(PlotterArtist, MeshArtist): Class Attributes ---------------- - default_vertexsize : int - default_edgewidth : float zorder_vertices : int zorder_edges : int zorder_faces : int """ - default_vertexsize: int = 5 - default_edgewidth: float = 1.0 - zorder_faces: int = 1000 zorder_edges: int = 2000 zorder_vertices: int = 3000 @@ -86,32 +81,35 @@ def __init__(self, super().__init__(mesh=mesh, **kwargs) - self._edge_width = 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.sizepolicy = sizepolicy self.vertices = vertices self.edges = edges self.faces = faces - self.vertex_color = vertexcolor + self.vertex_size = vertexsize self.edge_color = edgecolor - self.face_color = facecolor 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 + @property + 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 item(self): @@ -122,28 +120,13 @@ def item(self): def item(self, item: Mesh): self.mesh = item - @property - def edge_width(self) -> Dict[Tuple[int, int], float]: - """dict: Edge widths.""" - 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: Union[float, Dict[Tuple[int, int], float]]): - 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 data(self) -> List[List[float]]: return self.mesh.vertices_attributes('xy') - def clear(self): - self.clear_vertices() - self.clear_edges() - self.clear_faces() + # ============================================================================== + # clear and draw + # ============================================================================== def clear_vertices(self) -> None: if self._vertexcollection: @@ -196,9 +179,6 @@ def draw(self, if self.show_faces: self.draw_faces(faces=faces, color=facecolor) - def redraw(self) -> None: - raise NotImplementedError - def draw_vertices(self, vertices: Optional[List[int]] = None, color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, @@ -218,23 +198,17 @@ def draw_vertices(self, None """ self.clear_vertices() - if vertices: self.vertices = vertices if color: self.vertex_color = color - if self.sizepolicy == 'absolute': - size = self.vertexsize / self.plotter.dpi - else: - size = self.vertexsize / self.mesh.number_of_vertices() - circles = [] for vertex in self.vertices: x, y = self.vertex_xyz[vertex][:2] circle = Circle( [x, y], - radius=size, + 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, @@ -269,7 +243,6 @@ def draw_edges(self, None """ self.clear_edges() - if edges: self.edges = edges if color: @@ -313,7 +286,6 @@ def draw_faces(self, None """ self.clear_faces() - if faces: self.faces = faces if color: diff --git a/src/compas_plotters/artists/networkartist.py b/src/compas_plotters/artists/networkartist.py index e9c7b51d51e0..415609068de1 100644 --- a/src/compas_plotters/artists/networkartist.py +++ b/src/compas_plotters/artists/networkartist.py @@ -53,9 +53,6 @@ class NetworkArtist(PlotterArtist): zorder_edges : int """ - default_nodesize: int = 5 - default_edgewidth: float = 1.0 - zorder_edges: int = 2000 zorder_nodes: int = 3000 @@ -74,19 +71,15 @@ def __init__(self, super().__init__(network=network, **kwargs) - self._nodecollection = None - self._edgecollection = None - self._edge_width = None - 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 @property @@ -98,27 +91,13 @@ def item(self): def item(self, item: Network): self.network = item - @property - def edge_width(self) -> Dict[Tuple[int, int], float]: - """dict: Edge widths.""" - return self._edge_width - - @edge_width.setter - def edge_width(self, edgewidth: Union[float, Dict[Tuple[int, int], float]]): - if isinstance(edgewidth, dict): - self._edge_width = edgewidth - elif isinstance(edgewidth, (int, float)): - self._edge_width = {edge: edgewidth for edge in self.network.edges()} - else: - self._edge_width = {} - @property def data(self) -> List[List[float]]: return self.network.nodes_attributes('xy') - def clear(self): - self.clear_nodes() - self.clear_edges() + # ============================================================================== + # clear and draw + # ============================================================================== def clear_nodes(self): if self._nodecollection: @@ -156,9 +135,6 @@ def draw(self, if self.show_edges: self.draw_edges(edges=edges, color=edgecolor) - def redraw(self) -> None: - raise NotImplementedError - def draw_nodes(self, nodes: Optional[List[int]] = None, color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None) -> None: @@ -173,23 +149,17 @@ def draw_nodes(self, The color specification for the nodes. """ self.clear_nodes() - if nodes: self.nodes = nodes if color: self.node_color = color - if self.sizepolicy == 'absolute': - size = self.nodesize / self.plotter.dpi - else: - size = self.nodesize / self.network.number_of_nodes() - circles = [] for node in self.nodes: x, y = self.node_xyz[node][:2] circle = Circle( [x, y], - radius=size, + 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, @@ -219,7 +189,6 @@ def draw_edges(self, The color specification for the edges. """ self.clear_edges() - if edges: self.edges = edges if color: diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index ccff9fdc5867..57adddbe49ec 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -49,11 +49,9 @@ def __init__(self, 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 From 2609c18c7244e6ec658d38f1ff0f6b144782242f Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 5 Oct 2021 09:17:07 +0200 Subject: [PATCH 53/71] docs consistency --- src/compas_blender/artists/__init__.py | 43 ++++++++++++++++++++----- src/compas_ghpython/artists/__init__.py | 24 +++++++------- src/compas_plotters/artists/__init__.py | 17 ++++++++-- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index b687a287b942..904b9bab8083 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -8,31 +8,58 @@ Artists for visualising (painting) COMPAS data structures in Blender. -Base Classes -============ +Primitive Artists +================= .. autosummary:: :toctree: generated/ - BlenderArtist + FrameArtist -Classes -======= +Shape Artists +============= .. autosummary:: :toctree: generated/ + :nosignatures: BoxArtist CapsuleArtist ConeArtist CylinderArtist - FrameArtist + SphereArtist + PolyhedronArtist + + +Datastructure Artists +===================== + +.. autosummary:: + :toctree: generated/ + :nosignatures: + NetworkArtist MeshArtist - PolyhedronArtist + + +Robot Artist +============ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + RobotModelArtist - SphereArtist + + +Base Classes +============ + +.. autosummary:: + :toctree: generated/ + + BlenderArtist """ import inspect diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index 4a0a64c78364..4148a420e01d 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -6,18 +6,8 @@ .. currentmodule:: compas_ghpython.artists -Base Classes -============ - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - GHArtist - - -Geometry Artists -================ +Primitive Artists +================= .. autosummary:: :toctree: generated/ @@ -51,6 +41,16 @@ RobotModelArtist + +Base Classes +============ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + GHArtist + """ from __future__ import absolute_import diff --git a/src/compas_plotters/artists/__init__.py b/src/compas_plotters/artists/__init__.py index 7dbf589b4b90..4cc5be3876e7 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,6 +31,16 @@ MeshArtist NetworkArtist + +Base Classes +============ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + PlotterArtist + """ import inspect From 56b6737fd857b53016599c7b27f5b069fb77faef Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 5 Oct 2021 09:17:30 +0200 Subject: [PATCH 54/71] bug fix in cylinder artist parameters --- src/compas_blender/artists/cylinderartist.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/compas_blender/artists/cylinderartist.py b/src/compas_blender/artists/cylinderartist.py index d361322b99a4..0193e31f1dd5 100644 --- a/src/compas_blender/artists/cylinderartist.py +++ b/src/compas_blender/artists/cylinderartist.py @@ -28,7 +28,7 @@ def __init__(self, super().__init__(shape=cylinder, collection=collection or cylinder.name, **kwargs) - def draw(self, u=None, v=None): + def draw(self, u=None): """Draw the cylinder associated with the artist. Parameters @@ -36,9 +36,6 @@ def draw(self, u=None, v=None): u : int, optional Number of faces in the "u" direction. Default is ``~CylinderArtist.u``. - v : int, optional - Number of faces in the "v" direction. - Default is ``~CylinderArtist.v``. Returns ------- @@ -46,7 +43,6 @@ def draw(self, u=None, v=None): The objects created in Blender. """ u = u or self.u - v = v or self.v - vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) + vertices, faces = self.shape.to_vertices_and_faces(u=u) obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=self.color, collection=self.collection) return [obj] From 5edf1ce29f8a31419c6a2687f35e46ed403eb9ae Mon Sep 17 00:00:00 2001 From: Gonzalo Casas Date: Thu, 7 Oct 2021 01:26:02 +0200 Subject: [PATCH 55/71] Allow runtime arbitrary selectors for plugins --- docs/devguide.rst | 8 +++++--- src/compas/plugins.py | 17 +++++++++++++---- src/compas_ghpython/__init__.py | 6 +++++- src/compas_ghpython/artists/__init__.py | 14 ++++++++++++-- src/compas_rhino/artists/__init__.py | 14 ++++++++++++-- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/docs/devguide.rst b/docs/devguide.rst index 23a4b77dd4c7..c74b93fcea51 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 packages name string (e.g. ``requires=['scipy']``) or + a ``callable``, 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/src/compas/plugins.py b/src/compas/plugins.py index 1c1f10db1ee1..f0c3ef6c054a 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 packages + name (``str``) or a ``callable``, 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_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 4148a420e01d..c5684f67ad71 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -99,7 +99,17 @@ VolMeshArtist.default_cellcolor = (255, 0, 0) -@plugin(category='factories', pluggable_name='new_artist', requires=['ghpythonlib']) +def verify_gh_context(): + try: + import Rhino + import scriptcontext as sc + + return not isinstance(sc.doc, Rhino.RhinoDoc) + except: + return 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 @@ -127,7 +137,7 @@ def new_artist_gh(cls, *args, **kwargs): for name, value in inspect.getmembers(cls): if inspect.ismethod(value): if hasattr(value, '__isabstractmethod__'): - raise Exception('Abstract method not implemented') + raise Exception('Abstract method not implemented: {}'.format(value)) return super(Artist, cls).__new__(cls) diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 2a0b2e1d1ae4..e7465eca66c3 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -141,7 +141,17 @@ VolMeshArtist.default_cellcolor = (255, 0, 0) -@plugin(category='factories', pluggable_name='new_artist', requires=['Rhino']) +def verify_rhino_context(): + try: + import Rhino + import scriptcontext as sc + + return isinstance(sc.doc, Rhino.RhinoDoc) + except: + return False + + +@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 @@ -180,7 +190,7 @@ def new_artist_rhino(cls, *args, **kwargs): for name, value in inspect.getmembers(cls): if inspect.ismethod(value): if hasattr(value, '__isabstractmethod__'): - raise Exception('Abstract method not implemented') + raise Exception('Abstract method not implemented: {}'.format(value)) return super(Artist, cls).__new__(cls) From cd2eafdcdebb78b5e6f531e611005b5ab58f5200 Mon Sep 17 00:00:00 2001 From: Gonzalo Casas Date: Thu, 7 Oct 2021 01:33:10 +0200 Subject: [PATCH 56/71] Register robot model artist for Ghpython --- src/compas_ghpython/artists/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index c5684f67ad71..fc2a1c5b08b1 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -71,6 +71,8 @@ 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 @@ -121,6 +123,7 @@ def new_artist_gh(cls, *args, **kwargs): GHArtist.register(Mesh, MeshArtist) GHArtist.register(Network, NetworkArtist) GHArtist.register(VolMesh, VolMeshArtist) + GHArtist.register(RobotModel, RobotModelArtist) data = args[0] From 34a73b7bb3c2ce1efc7188913e9a1f9c23c83710 Mon Sep 17 00:00:00 2001 From: Gonzalo Casas Date: Thu, 7 Oct 2021 15:31:34 +0200 Subject: [PATCH 57/71] review suggestions --- docs/devguide.rst | 4 ++-- src/compas/plugins.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/devguide.rst b/docs/devguide.rst index c74b93fcea51..2ecdc1aa6cda 100644 --- a/docs/devguide.rst +++ b/docs/devguide.rst @@ -393,8 +393,8 @@ There are a few additional options that plugins can use: * ``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 different criteria. - The requirement can either be a packages name string (e.g. ``requires=['scipy']``) or - a ``callable``, in which any arbitrary check can be implemented + 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 diff --git a/src/compas/plugins.py b/src/compas/plugins.py index f0c3ef6c054a..964e92e9efe1 100644 --- a/src/compas/plugins.py +++ b/src/compas/plugins.py @@ -323,9 +323,9 @@ def plugin(method=None, category=None, requires=None, tryfirst=False, trylast=Fa An optional string to group or categorize plugins. 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 packages - name (``str``) or a ``callable``, in which any arbitrary check can be - implemented. + 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``. From 9d4be79fe0814f27466b3b54cd87ab5a1b08e54b Mon Sep 17 00:00:00 2001 From: Gonzalo Casas Date: Thu, 7 Oct 2021 15:36:57 +0200 Subject: [PATCH 58/71] lint --- src/compas_ghpython/artists/__init__.py | 2 +- src/compas_rhino/artists/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index fc2a1c5b08b1..c70b691d6aa5 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -107,7 +107,7 @@ def verify_gh_context(): import scriptcontext as sc return not isinstance(sc.doc, Rhino.RhinoDoc) - except: + except: # noqa: E722 return False diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index e7465eca66c3..4584e4d9a9a5 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -147,7 +147,7 @@ def verify_rhino_context(): import scriptcontext as sc return isinstance(sc.doc, Rhino.RhinoDoc) - except: + except: # noqa: E722 return False From 18e89141cff03dc3b81d7052b012c265d1cf43b9 Mon Sep 17 00:00:00 2001 From: brgcode Date: Thu, 7 Oct 2021 22:04:52 +0200 Subject: [PATCH 59/71] overwrite all options in draw call --- src/compas/artists/shapeartist.py | 8 +++++--- src/compas_blender/artists/boxartist.py | 10 ++++++++-- src/compas_blender/artists/capsuleartist.py | 7 +++++-- src/compas_blender/artists/coneartist.py | 7 +++++-- src/compas_blender/artists/cylinderartist.py | 7 +++++-- src/compas_blender/artists/polyhedronartist.py | 10 ++++++++-- src/compas_blender/artists/sphereartist.py | 7 +++++-- src/compas_blender/artists/torusartist.py | 7 +++++-- 8 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/compas/artists/shapeartist.py b/src/compas/artists/shapeartist.py index e2e2c433988b..097a05162dcd 100644 --- a/src/compas/artists/shapeartist.py +++ b/src/compas/artists/shapeartist.py @@ -38,13 +38,15 @@ class ShapeArtist(Artist): default_color = (1, 1, 1) def __init__(self, shape, color=None, **kwargs): - super(ShapeArtist, self).__init__(**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): @@ -73,7 +75,7 @@ def u(self): @u.setter def u(self, u): - if u > 3: + if u and u > 3: self._u = u @property @@ -84,5 +86,5 @@ def v(self): @v.setter def v(self, v): - if v > 3: + if v and v > 3: self._v = v diff --git a/src/compas_blender/artists/boxartist.py b/src/compas_blender/artists/boxartist.py index 3b1ddecdeaa3..7cad39fb2a4f 100644 --- a/src/compas_blender/artists/boxartist.py +++ b/src/compas_blender/artists/boxartist.py @@ -27,14 +27,20 @@ def __init__(self, super().__init__(shape=box, collection=collection or box.name, **kwargs) - def draw(self): + 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=self.color, collection=self.collection) + 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 index 80dc78bd6d30..0921104b555c 100644 --- a/src/compas_blender/artists/capsuleartist.py +++ b/src/compas_blender/artists/capsuleartist.py @@ -28,11 +28,13 @@ def __init__(self, super().__init__(shape=capsule, collection=collection or capsule.name, **kwargs) - def draw(self, u=None, v=None): + 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``. @@ -47,6 +49,7 @@ def draw(self, u=None, v=None): """ 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=self.color, collection=self.collection) + 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 index 632d2eafec32..41f968fbd079 100644 --- a/src/compas_blender/artists/coneartist.py +++ b/src/compas_blender/artists/coneartist.py @@ -28,11 +28,13 @@ def __init__(self, super().__init__(shape=cone, collection=collection or cone.name, **kwargs) - def draw(self, u=None): + 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``. @@ -43,6 +45,7 @@ def draw(self, u=None): 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=self.color, collection=self.collection) + 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 index 0193e31f1dd5..d74d4615dbe2 100644 --- a/src/compas_blender/artists/cylinderartist.py +++ b/src/compas_blender/artists/cylinderartist.py @@ -28,11 +28,13 @@ def __init__(self, super().__init__(shape=cylinder, collection=collection or cylinder.name, **kwargs) - def draw(self, u=None): + 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``. @@ -43,6 +45,7 @@ def draw(self, u=None): 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=self.color, collection=self.collection) + 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/polyhedronartist.py b/src/compas_blender/artists/polyhedronartist.py index a41360202fc2..79561ea07bc3 100644 --- a/src/compas_blender/artists/polyhedronartist.py +++ b/src/compas_blender/artists/polyhedronartist.py @@ -27,14 +27,20 @@ def __init__(self, super().__init__(shape=polyhedron, collection=collection or polyhedron.name, **kwargs) - def draw(self): + 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=self.color, collection=self.collection) + 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/sphereartist.py b/src/compas_blender/artists/sphereartist.py index 3d14b7a93f2f..f14edb4d9c25 100644 --- a/src/compas_blender/artists/sphereartist.py +++ b/src/compas_blender/artists/sphereartist.py @@ -28,11 +28,13 @@ def __init__(self, super().__init__(shape=sphere, collection=collection or sphere.name, **kwargs) - def draw(self, u=None, v=None): + 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``. @@ -47,6 +49,7 @@ def draw(self, u=None, v=None): """ 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=self.color, collection=self.collection) + 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 index c04553dc875a..1979e025116f 100644 --- a/src/compas_blender/artists/torusartist.py +++ b/src/compas_blender/artists/torusartist.py @@ -28,11 +28,13 @@ def __init__(self, super().__init__(shape=torus, collection=collection or torus.name, **kwargs) - def draw(self, u=None, v=None): + 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``. @@ -47,6 +49,7 @@ def draw(self, u=None, v=None): """ 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=self.color, collection=self.collection) + obj = compas_blender.draw_mesh(vertices, faces, name=self.shape.name, color=color, collection=self.collection) return [obj] From 359887af5cc5ecc0b2bcb1598fefb704a79831c2 Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 09:04:15 +0200 Subject: [PATCH 60/71] remove debug print --- src/compas_plotters/plotter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compas_plotters/plotter.py b/src/compas_plotters/plotter.py index 118f9f610769..303a1752f995 100644 --- a/src/compas_plotters/plotter.py +++ b/src/compas_plotters/plotter.py @@ -220,7 +220,6 @@ def zoom_extents(self, padding: Optional[int] = None) -> None: width, height = self.figsize fig_aspect = width / height data = [] - print(self.artists) for artist in self.artists: data += artist.data x, y = zip(* data) From 1d476c45f49142e1aeb4f756979160e3a9bfdfee Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 10:15:09 +0200 Subject: [PATCH 61/71] rhino box artist faces only --- src/compas_rhino/artists/boxartist.py | 37 ++++++++------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/compas_rhino/artists/boxartist.py b/src/compas_rhino/artists/boxartist.py index 13eabe2b1a21..57ee677a9b42 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -21,40 +21,25 @@ class BoxArtist(RhinoArtist, ShapeArtist): def __init__(self, box, layer=None, **kwargs): super(BoxArtist, self).__init__(shape=box, layer=layer, **kwargs) - def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + 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.shape.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.shape.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.shape.name, color=self.color, disjoint=True) - guids.append(guid) - else: - polygons = [{'points': [vertices[index] for index in face], 'color': self.color, 'name': self.shape.name} for face in faces] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) - return guids + polygons = [{'points': [vertices[index] for index in face]} for face in self.shape.faces] + guids = compas_rhino.draw_faces(polygons, 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, self.shape.name) + compas_rhino.rs.ObjectColor(guid, color) + return [guid] From ce4a93fed650948cf5f1245884533536fa91579a Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 10:22:23 +0200 Subject: [PATCH 62/71] default to redraw false --- src/compas_rhino/utilities/drawing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From b0588db168a48f9f47e152ad8bdbc86057f34585 Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 10:22:38 +0200 Subject: [PATCH 63/71] capsule and box as disjoint mesh --- src/compas_rhino/artists/boxartist.py | 13 +++---- src/compas_rhino/artists/capsuleartist.py | 43 ++++++----------------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/compas_rhino/artists/boxartist.py b/src/compas_rhino/artists/boxartist.py index 57ee677a9b42..c23656b87dc5 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -36,10 +36,11 @@ def draw(self, color=None): """ color = color or self.color vertices = [list(vertex) for vertex in self.shape.vertices] - polygons = [{'points': [vertices[index] for index in face]} for face in self.shape.faces] - guids = compas_rhino.draw_faces(polygons, 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, self.shape.name) - compas_rhino.rs.ObjectColor(guid, color) + 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 e5e2b6789245..09ecbbf7fce3 100644 --- a/src/compas_rhino/artists/capsuleartist.py +++ b/src/compas_rhino/artists/capsuleartist.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist from .artist import RhinoArtist @@ -22,54 +21,34 @@ class CapsuleArtist(RhinoArtist, ShapeArtist): def __init__(self, capsule, layer=None, **kwargs): super(CapsuleArtist, self).__init__(shape=capsule, layer=layer, **kwargs) - def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + 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``. - 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``. 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.shape.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) - return guids + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] From 1dcdc1090e7dc178627704c49408a890e4936e2b Mon Sep 17 00:00:00 2001 From: Tom Van Mele Date: Fri, 8 Oct 2021 10:45:31 +0200 Subject: [PATCH 64/71] Update CHANGELOG.md Co-authored-by: beverlylytle <57254617+beverlylytle@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa297e8febf..ed866ee8d270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * 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 `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. From fe9e997fd3f2a769cd69920d959b049ddc8e4741 Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 10:55:19 +0200 Subject: [PATCH 65/71] rhino shapes on par with blender implementation --- src/compas_rhino/artists/coneartist.py | 43 +++++--------------- src/compas_rhino/artists/cylinderartist.py | 43 +++++--------------- src/compas_rhino/artists/polyhedronartist.py | 38 ++++++----------- src/compas_rhino/artists/sphereartist.py | 43 +++++--------------- src/compas_rhino/artists/torusartist.py | 43 +++++--------------- 5 files changed, 56 insertions(+), 154 deletions(-) diff --git a/src/compas_rhino/artists/coneartist.py b/src/compas_rhino/artists/coneartist.py index 168424b4c9e7..288b230e325c 100644 --- a/src/compas_rhino/artists/coneartist.py +++ b/src/compas_rhino/artists/coneartist.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist from .artist import RhinoArtist @@ -23,50 +22,30 @@ class ConeArtist(RhinoArtist, ShapeArtist): def __init__(self, cone, layer=None, **kwargs): super(ConeArtist, self).__init__(shape=cone, layer=layer, **kwargs) - def draw(self, u=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + 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``. - 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``. 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.shape.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) - 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 a5d052777013..a893b7df37c1 100644 --- a/src/compas_rhino/artists/cylinderartist.py +++ b/src/compas_rhino/artists/cylinderartist.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist from .artist import RhinoArtist @@ -22,50 +21,30 @@ class CylinderArtist(RhinoArtist, ShapeArtist): def __init__(self, cylinder, layer=None, **kwargs): super(CylinderArtist, self).__init__(shape=cylinder, layer=layer, **kwargs) - def draw(self, u=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + 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``. - 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``. 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.shape.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) - 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/polyhedronartist.py b/src/compas_rhino/artists/polyhedronartist.py index 664970bb552d..e185318c5987 100644 --- a/src/compas_rhino/artists/polyhedronartist.py +++ b/src/compas_rhino/artists/polyhedronartist.py @@ -21,40 +21,26 @@ class PolyhedronArtist(RhinoArtist, ShapeArtist): def __init__(self, polyhedron, layer=None, **kwargs): super(PolyhedronArtist, self).__init__(shape=polyhedron, layer=layer, **kwargs) - def draw(self, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + 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.shape.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) - 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/sphereartist.py b/src/compas_rhino/artists/sphereartist.py index c6da0da04e9c..dc85aa0298b5 100644 --- a/src/compas_rhino/artists/sphereartist.py +++ b/src/compas_rhino/artists/sphereartist.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist from .artist import RhinoArtist @@ -22,54 +21,34 @@ class SphereArtist(RhinoArtist, ShapeArtist): def __init__(self, sphere, layer=None, **kwargs): super(SphereArtist, self).__init__(shape=sphere, layer=layer, **kwargs) - def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + 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``. - 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``. 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.shape.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) - 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 cfe427ce007b..1db1da2b6847 100644 --- a/src/compas_rhino/artists/torusartist.py +++ b/src/compas_rhino/artists/torusartist.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas.utilities import pairwise import compas_rhino from compas.artists import ShapeArtist from .artist import RhinoArtist @@ -22,54 +21,34 @@ class TorusArtist(RhinoArtist, ShapeArtist): def __init__(self, torus, layer=None, **kwargs): super(TorusArtist, self).__init__(shape=torus, layer=layer, **kwargs) - def draw(self, u=None, v=None, show_vertices=False, show_edges=False, show_faces=True, join_faces=True): + 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``. - 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``. 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.shape.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) - return guids + guid = compas_rhino.draw_mesh(vertices, + faces, + layer=self.layer, + name=self.shape.name, + color=color, + disjoint=True) + return [guid] From dffc7b76c706d4fd62f5b59e94c090b18ffde8ef Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 11:33:55 +0200 Subject: [PATCH 66/71] clear and redraw as pluggable drawing utils --- src/compas/artists/artist.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/compas/artists/artist.py b/src/compas/artists/artist.py index 3464ae524d9c..4eab45234006 100644 --- a/src/compas/artists/artist.py +++ b/src/compas/artists/artist.py @@ -6,6 +6,16 @@ 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 @@ -20,6 +30,14 @@ class Artist(object): 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 From 5cbb2b800756f9839479670a90e0098078e11ed4 Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 11:34:09 +0200 Subject: [PATCH 67/71] rhino implementation for clear and redraw --- src/compas_rhino/artists/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 4584e4d9a9a5..2af233e3cc06 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -102,6 +102,7 @@ from compas.datastructures import VolMesh from compas.robots import RobotModel +import compas_rhino from .artist import RhinoArtist from .circleartist import CircleArtist @@ -151,6 +152,16 @@ def verify_rhino_context(): return 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 From 7a16821df45609a5b06279761378ac4244185387 Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 13:15:03 +0200 Subject: [PATCH 68/71] register artists only once --- src/compas_ghpython/artists/__init__.py | 26 ++++++++------ src/compas_plotters/artists/__init__.py | 37 ++++++++++++++------ src/compas_rhino/artists/__init__.py | 46 ++++++++++++++----------- 3 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index c70b691d6aa5..170b3f2d7c95 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -111,19 +111,25 @@ def verify_gh_context(): 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 - - 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) + 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] diff --git a/src/compas_plotters/artists/__init__.py b/src/compas_plotters/artists/__init__.py index 4cc5be3876e7..950a9adcc859 100644 --- a/src/compas_plotters/artists/__init__.py +++ b/src/compas_plotters/artists/__init__.py @@ -71,19 +71,34 @@ from .networkartist import NetworkArtist -@plugin(category='factories', pluggable_name='new_artist', trylast=True, requires=['matplotlib']) +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 - - 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) + 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] diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 2af233e3cc06..4675726a820c 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -152,6 +152,9 @@ def verify_rhino_context(): return False +artists_registered = False + + @plugin(category='drawing-utils', pluggable_name='clear', requires=['Rhino', verify_rhino_context]) def clear_rhino(): compas_rhino.clear() @@ -165,26 +168,29 @@ def redraw_rhino(): @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 - - 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) + 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] From 50cbead3c17aa069fbb2775a35f5b3159fa2f8fb Mon Sep 17 00:00:00 2001 From: brgcode Date: Fri, 8 Oct 2021 13:15:16 +0200 Subject: [PATCH 69/71] redraw and clear plugins --- src/compas_blender/__init__.py | 2 +- src/compas_blender/artists/__init__.py | 42 ++++++++++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/compas_blender/__init__.py b/src/compas_blender/__init__.py index 3ac511bc5b06..52ec393fe32d 100644 --- a/src/compas_blender/__init__.py +++ b/src/compas_blender/__init__.py @@ -42,7 +42,7 @@ def clear(): bpy.data.collections.remove(block) -def redraw(self): +def redraw(): """Trigger a redraw.""" bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 904b9bab8083..dd2f95e4f939 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -64,6 +64,8 @@ """ import inspect +import compas_blender + from compas.plugins import plugin from compas.artists import Artist from compas.artists import DataArtistNotRegistered @@ -94,21 +96,37 @@ 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 - - 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) + 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] From 072f2006ccdd0a8cb8c886f631df2437bdde435a Mon Sep 17 00:00:00 2001 From: brgcode Date: Tue, 12 Oct 2021 08:10:18 +0200 Subject: [PATCH 70/71] variable zstack options --- src/compas/artists/primitiveartist.py | 2 +- src/compas_plotters/artists/circleartist.py | 4 ++-- src/compas_plotters/artists/ellipseartist.py | 4 ++-- src/compas_plotters/artists/lineartist.py | 4 ++-- src/compas_plotters/artists/meshartist.py | 18 ++++++++++++++---- src/compas_plotters/artists/networkartist.py | 14 ++++++++++---- src/compas_plotters/artists/pointartist.py | 4 ++-- src/compas_plotters/artists/polygonartist.py | 4 ++-- src/compas_plotters/artists/polylineartist.py | 4 ++-- src/compas_plotters/artists/segmentartist.py | 4 ++-- src/compas_plotters/artists/vectorartist.py | 6 +++--- src/compas_plotters/plotter.py | 11 +++++++++-- 12 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/compas/artists/primitiveartist.py b/src/compas/artists/primitiveartist.py index a8ff4634ad61..a3d0328cc8d4 100644 --- a/src/compas/artists/primitiveartist.py +++ b/src/compas/artists/primitiveartist.py @@ -33,7 +33,7 @@ class PrimitiveArtist(Artist): default_color = (0, 0, 0) def __init__(self, primitive, color=None, **kwargs): - super(PrimitiveArtist, self).__init__(**kwargs) + super(PrimitiveArtist, self).__init__() self._primitive = None self._color = None self.primitive = primitive diff --git a/src/compas_plotters/artists/circleartist.py b/src/compas_plotters/artists/circleartist.py index c239f9653c17..672aee59c0af 100644 --- a/src/compas_plotters/artists/circleartist.py +++ b/src/compas_plotters/artists/circleartist.py @@ -15,8 +15,6 @@ class CircleArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS circles.""" - zorder: int = 1000 - def __init__(self, circle: Circle, linewidth: float = 1.0, @@ -25,6 +23,7 @@ def __init__(self, edgecolor: Color = (0, 0, 0), fill: bool = True, alpha: float = 1.0, + zorder: int = 1000, **kwargs: Any): super().__init__(primitive=circle, **kwargs) @@ -36,6 +35,7 @@ def __init__(self, self.edgecolor = edgecolor self.fill = fill self.alpha = alpha + self.zorder = zorder @property def circle(self): diff --git a/src/compas_plotters/artists/ellipseartist.py b/src/compas_plotters/artists/ellipseartist.py index 5acd0fbb09d8..8a713527da6a 100644 --- a/src/compas_plotters/artists/ellipseartist.py +++ b/src/compas_plotters/artists/ellipseartist.py @@ -15,8 +15,6 @@ class EllipseArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS ellipses.""" - zorder: int = 1000 - def __init__(self, ellipse: Ellipse, linewidth: float = 1.0, @@ -25,6 +23,7 @@ def __init__(self, edgecolor: Color = (0, 0, 0), fill: bool = True, alpha: float = 1.0, + zorder: int = 1000, **kwargs: Any): super().__init__(primitive=ellipse, **kwargs) @@ -36,6 +35,7 @@ def __init__(self, self.edgecolor = edgecolor self.fill = fill self.alpha = alpha + self.zorder = zorder @property def ellipse(self): diff --git a/src/compas_plotters/artists/lineartist.py b/src/compas_plotters/artists/lineartist.py index 13c00ab62557..0eb44229dbb3 100644 --- a/src/compas_plotters/artists/lineartist.py +++ b/src/compas_plotters/artists/lineartist.py @@ -16,8 +16,6 @@ class LineArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS lines.""" - zorder: int = 1000 - def __init__(self, line: Line, draw_points: bool = False, @@ -25,6 +23,7 @@ def __init__(self, linewidth: float = 1.0, linestyle: Literal['solid', 'dotted', 'dashed', 'dashdot'] = 'solid', color: Color = (0, 0, 0), + zorder: int = 1000, **kwargs: Any): super().__init__(primitive=line, **kwargs) @@ -38,6 +37,7 @@ def __init__(self, self.linewidth = linewidth self.linestyle = linestyle self.color = color + self.zorder = zorder @property def line(self): diff --git a/src/compas_plotters/artists/meshartist.py b/src/compas_plotters/artists/meshartist.py index bc7605fb7aad..6bf4aac88eb3 100644 --- a/src/compas_plotters/artists/meshartist.py +++ b/src/compas_plotters/artists/meshartist.py @@ -59,10 +59,6 @@ class MeshArtist(PlotterArtist, MeshArtist): zorder_faces : int """ - zorder_faces: int = 1000 - zorder_edges: int = 2000 - zorder_vertices: int = 3000 - def __init__(self, mesh: Mesh, vertices: Optional[List[int]] = None, @@ -77,6 +73,7 @@ def __init__(self, show_faces: bool = True, vertexsize: int = 5, sizepolicy: Literal['relative', 'absolute'] = 'relative', + zorder: int = 1000, **kwargs: Any): super().__init__(mesh=mesh, **kwargs) @@ -94,6 +91,7 @@ def __init__(self, self.show_vertices = show_vertices self.show_edges = show_edges self.show_faces = show_faces + self.zorder = zorder @property def vertex_size(self): @@ -111,6 +109,18 @@ def vertex_size(self, vertexsize): elif isinstance(vertexsize, (int, float)): self._vertex_size = {vertex: vertexsize / factor for vertex in self.mesh.vertices()} + @property + def zorder_faces(self): + return self.zorder + 10 + + @property + def zorder_edges(self): + return self.zorder + 20 + + @property + def zorder_vertices(self): + return self.zorder + 30 + @property def item(self): """Mesh: Alias for ``~MeshArtist.mesh``""" diff --git a/src/compas_plotters/artists/networkartist.py b/src/compas_plotters/artists/networkartist.py index 415609068de1..3d29e2408c92 100644 --- a/src/compas_plotters/artists/networkartist.py +++ b/src/compas_plotters/artists/networkartist.py @@ -53,9 +53,6 @@ class NetworkArtist(PlotterArtist): zorder_edges : int """ - zorder_edges: int = 2000 - zorder_nodes: int = 3000 - def __init__(self, network: Network, nodes: Optional[List[int]] = None, @@ -67,6 +64,7 @@ def __init__(self, show_edges: bool = True, nodesize: int = 5, sizepolicy: Literal['relative', 'absolute'] = 'relative', + zorder: int = 2000, **kwargs): super().__init__(network=network, **kwargs) @@ -79,8 +77,16 @@ def __init__(self, self.edge_width = edgewidth self.show_nodes = show_nodes self.show_edges = show_edges - self.sizepolicy = sizepolicy + self.zorder = zorder + + @property + def zorder_edges(self): + return self.zorder + + @property + def zorder_nodes(self): + return self.zorder + 10 @property def item(self): diff --git a/src/compas_plotters/artists/pointartist.py b/src/compas_plotters/artists/pointartist.py index 5cd1ac55f095..f71e5ea1ec66 100644 --- a/src/compas_plotters/artists/pointartist.py +++ b/src/compas_plotters/artists/pointartist.py @@ -15,13 +15,12 @@ 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), + zorder: int = 9000, **kwargs: Any): super().__init__(primitive=point, **kwargs) @@ -31,6 +30,7 @@ def __init__(self, self.size = size self.facecolor = facecolor self.edgecolor = edgecolor + self.zorder = zorder @property def point(self): diff --git a/src/compas_plotters/artists/polygonartist.py b/src/compas_plotters/artists/polygonartist.py index da4bf67fb30f..58d32767a9f5 100644 --- a/src/compas_plotters/artists/polygonartist.py +++ b/src/compas_plotters/artists/polygonartist.py @@ -15,8 +15,6 @@ class PolygonArtist(PlotterArtist, PrimitiveArtist): """Artist for COMPAS polygons.""" - zorder: int = 1000 - def __init__(self, polygon: Polygon, linewidth: float = 1.0, @@ -25,6 +23,7 @@ def __init__(self, edgecolor: Color = (0, 0, 0), fill: bool = True, alpha: float = 1.0, + zorder: int = 1000, **kwargs: Any): super().__init__(primitive=polygon, **kwargs) @@ -36,6 +35,7 @@ def __init__(self, self.edgecolor = edgecolor self.fill = fill self.alpha = alpha + self.zorder = zorder @property def polygon(self): diff --git a/src/compas_plotters/artists/polylineartist.py b/src/compas_plotters/artists/polylineartist.py index 6a96d2430516..68e9bc17980d 100644 --- a/src/compas_plotters/artists/polylineartist.py +++ b/src/compas_plotters/artists/polylineartist.py @@ -15,14 +15,13 @@ 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), + zorder: int = 1000, **kwargs: Any): super().__init__(primitive=polyline, **kwargs) @@ -33,6 +32,7 @@ def __init__(self, self.linewidth = linewidth self.linestyle = linestyle self.color = color + self.zorder = zorder @property def polyline(self): diff --git a/src/compas_plotters/artists/segmentartist.py b/src/compas_plotters/artists/segmentartist.py index 832e4bb9f145..da4ce6af3945 100644 --- a/src/compas_plotters/artists/segmentartist.py +++ b/src/compas_plotters/artists/segmentartist.py @@ -15,14 +15,13 @@ 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), + zorder: int = 2000, **kwargs: Any): super().__init__(primitive=line, **kwargs) @@ -34,6 +33,7 @@ def __init__(self, self.linestyle = linestyle self.linewidth = linewidth self.color = color + self.zorder = zorder @property def line(self): diff --git a/src/compas_plotters/artists/vectorartist.py b/src/compas_plotters/artists/vectorartist.py index 02bfb46433d3..a150831c00b3 100644 --- a/src/compas_plotters/artists/vectorartist.py +++ b/src/compas_plotters/artists/vectorartist.py @@ -16,13 +16,12 @@ 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), + zorder: int = 3000, **kwargs: Any): super().__init__(primitive=vector, **kwargs) @@ -32,6 +31,7 @@ def __init__(self, self.draw_point = draw_point self.point = point or Point(0.0, 0.0, 0.0) self.color = color + self.zorder = zorder @property def vector(self): @@ -46,7 +46,7 @@ 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/plotter.py b/src/compas_plotters/plotter.py index 303a1752f995..75af3852fa71 100644 --- a/src/compas_plotters/plotter.py +++ b/src/compas_plotters/plotter.py @@ -1,5 +1,6 @@ import os from typing import Callable, Optional, Tuple, List, Union +from typing_extensions import Literal import matplotlib import matplotlib.pyplot as plt import tempfile @@ -37,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 @@ -47,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]]: @@ -259,7 +262,11 @@ def add(self, """Add a COMPAS geometry object or data structure to the plot. """ if not artist: - artist = PlotterArtist(item, **kwargs) + 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 From a74776f765542439fa380438aad58d10be96aa89 Mon Sep 17 00:00:00 2001 From: Gonzalo Casas Date: Tue, 12 Oct 2021 15:32:36 +0200 Subject: [PATCH 71/71] Implement abstract methods on gh's MeshArtist --- src/compas_ghpython/artists/meshartist.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/compas_ghpython/artists/meshartist.py b/src/compas_ghpython/artists/meshartist.py index 6ac2720b1d8d..ae8a1dc23a4d 100644 --- a/src/compas_ghpython/artists/meshartist.py +++ b/src/compas_ghpython/artists/meshartist.py @@ -162,3 +162,15 @@ def draw_edges(self, edges=None, color=None): '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