diff --git a/.gitignore b/.gitignore index 9869b6b5561..ea0011a574b 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,5 @@ src/compas_ghpython/components/**/*.ghuser dev NOTES + +src/compas_blender/conversions/__temp/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 293a74758d2..475b0af5dd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `polyhedron_to_rhino` to `compas_rhino.conversions`. * Added `from_mesh` plugin to `compas_rhino.geometry.RhinoBrep`. * Added `compas.geometry.Plane.worldYZ` and `compas.geometry.Plane.worldZX`. +* Added `compas_rhino.conversions.brep_to_compas_box`. +* Added `compas_rhino.conversions.brep_to_compas_cone`. +* Added `compas_rhino.conversions.brep_to_compas_cylinder`. +* Added `compas_rhino.conversions.brep_to_compas_sphere`. +* Added `compas_rhino.conversions.brep_to_rhino`. +* Added `compas_rhino.conversions.capsule_to_rhino_brep`. +* Added `compas_rhino.conversions.cone_to_rhino_brep`. +* Added `compas_rhino.conversions.curve_to_rhino`. +* Added `compas_rhino.conversions.cylinder_to_rhino_brep`. +* Added `compas_rhino.conversions.extrusion_to_compas_box`. +* Added `compas_rhino.conversions.extrusion_to_rhino_cylinder`. +* Added `compas_rhino.conversions.extrusion_to_rhino_torus`. +* Added `compas_rhino.conversions.polyline_to_rhino_curve`. +* Added `compas_rhino.conversions.surface_to_compas`. +* Added `compas_rhino.conversions.surface_to_compas_mesh`. +* Added `compas_rhino.conversions.surface_to_compas_quadmesh`. +* Added `compas_rhino.conversions.surface_to_rhino`. +* Added `compas_rhino.conversions.torus_to_rhino_brep`. +* Added `compas_rhino.artists._helpers.attributes`. +* Added `compas_rhino.artists._helpers.ngon`. +* Added `compas.geometry.find_span`. +* Added `compas.geometry.construct_knotvector`. +* Added `compas.geometry.knotvector_to_knots_and_mults`. +* Added `compas.geometry.knots_and_mults_to_knotvector`. +* Added `compas.geometry.compute_basisfuncs`. +* Added `compas.geometry.compute_basisfuncsderivs`. +* Added `compas.geometry.DefaultNurbsCurve` as try-last, Python-only plugin for `compas.geometry.NurbsCurve`. +* Added `compas.geometry.DefaultNurbsSurface` as try-last, Python-only plugin for `compas.geometry.NurbsSurface`. +* Added color count to constructor functions of `compas.colors.ColorMap`. ### Changed @@ -174,6 +203,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed `compas.datastructures.Graph.delete_edge` to delete invalid (u, u) edges and not delete edges in opposite directions (v, u) * Fixed bug in `compas.datastructures.Mesh.insert_vertex`. * Fixed bug in `compas.geometry.angle_vectors_signed`. +* Changed `compas.artists.MeshArtist` default colors. ### Removed @@ -234,6 +264,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed classmethod `compas.color.Color.from_data`. * Removed `validate_data` from `compas.data.validators`. * Removed `json_validate` from `compas.data.json`. +* Removed `compas_rhino.conversions.Box`. +* Removed `compas_rhino.conversions.Circle`. +* Removed `compas_rhino.conversions.Cone`. +* Removed `compas_rhino.conversions.Curve`. +* Removed `compas_rhino.conversions.Cylinder`. +* Removed `compas_rhino.conversions.Ellipse`. +* Removed `compas_rhino.conversions.Line`. +* Removed `compas_rhino.conversions.Mesh`. +* Removed `compas_rhino.conversions.Plane`. +* Removed `compas_rhino.conversions.Point`. +* Removed `compas_rhino.conversions.Polyline`. +* Removed `compas_rhino.conversions.Vector`. +* Removed `compas_rhino.artists.NetworkArtist.draw_nodelabels`. +* Removed `compas_rhino.artists.NetworkArtist.draw_edgelabels`. +* Removed `compas_rhino.artists.MeshArtist.draw_vertexlabels`. +* Removed `compas_rhino.artists.MeshArtist.draw_edgelabels`. +* Removed `compas_rhino.artists.MeshArtist.draw_facelabels`. +* Removed `compas_rhino.artists.VolMeshArtist.draw_vertexlabels`. +* Removed `compas_rhino.artists.VolMeshArtist.draw_edgelabels`. +* Removed `compas_rhino.artists.VolMeshArtist.draw_facelabels`. +* Removed `compas_rhino.artists.VolMeshArtist.draw_celllabels`. ## [1.17.5] 2023-02-16 diff --git a/docs/userguide/tutorials/artists/boxartist.py b/docs/userguide/tutorials/artists/boxartist.py new file mode 100644 index 00000000000..915214f9008 --- /dev/null +++ b/docs/userguide/tutorials/artists/boxartist.py @@ -0,0 +1,11 @@ +from compas.geometry import Box +from compas.artists import Artist + +Artist.clear() + +box = Box.from_corner_corner_height([0, 0, 0], [1, 1, 0], 3) + +artist = Artist(box, layer='Test::Child') +artist.draw(color=(0.0, 1.0, 0.0)) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/capsuleartist.py b/docs/userguide/tutorials/artists/capsuleartist.py new file mode 100644 index 00000000000..da3b1933f98 --- /dev/null +++ b/docs/userguide/tutorials/artists/capsuleartist.py @@ -0,0 +1,11 @@ +from compas.geometry import Capsule +from compas.artists import Artist + +Artist.clear() + +capsule = Capsule(radius=1.0, height=7.0) + +artist = Artist(capsule) +artist.draw(color=(1.0, 0.0, 0.0)) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/circleartist.py b/docs/userguide/tutorials/artists/circleartist.py new file mode 100644 index 00000000000..66c33f8ee6d --- /dev/null +++ b/docs/userguide/tutorials/artists/circleartist.py @@ -0,0 +1,11 @@ +from compas.geometry import Circle +from compas.artists import Artist + +Artist.clear() + +circle = Circle(radius=3.0) + +artist = Artist(circle) +artist.draw(color=(0.0, 0.0, 1.0), show_point=True, show_normal=True) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/coneartist.py b/docs/userguide/tutorials/artists/coneartist.py new file mode 100644 index 00000000000..12113ec9d91 --- /dev/null +++ b/docs/userguide/tutorials/artists/coneartist.py @@ -0,0 +1,11 @@ +from compas.geometry import Cone +from compas.artists import Artist + +Artist.clear() + +cone = Cone(radius=1.0, height=7.0) + +artist = Artist(cone) +artist.draw(color=(1.0, 0.0, 0.0)) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/curveartist.py b/docs/userguide/tutorials/artists/curveartist.py new file mode 100644 index 00000000000..0ce569c4e5a --- /dev/null +++ b/docs/userguide/tutorials/artists/curveartist.py @@ -0,0 +1,12 @@ +from compas.geometry import NurbsCurve +from compas.artists import Artist + +Artist.clear() + +curve = NurbsCurve.from_points([[0, 0, 0], [3, 3, 6], [6, -3, -3], [9, 0, 0]]) + +artist = Artist(curve) +artist.color = (0.0, 1.0, 0.0) +artist.draw() + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/cylinderartist.py b/docs/userguide/tutorials/artists/cylinderartist.py new file mode 100644 index 00000000000..9628b035a34 --- /dev/null +++ b/docs/userguide/tutorials/artists/cylinderartist.py @@ -0,0 +1,11 @@ +from compas.geometry import Cylinder +from compas.artists import Artist + +Artist.clear() + +cylinder = Cylinder(radius=1.0, height=7.0) + +artist = Artist(cylinder) +artist.draw(color=(1.0, 0.0, 0.0)) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/frameartist.py b/docs/userguide/tutorials/artists/frameartist.py new file mode 100644 index 00000000000..7a2d41582f2 --- /dev/null +++ b/docs/userguide/tutorials/artists/frameartist.py @@ -0,0 +1,10 @@ +from compas.geometry import Frame +from compas.artists import Artist + +Artist.clear() + +frame = Frame.worldXY() +artist = Artist(frame) +artist.draw() + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/lineartist.py b/docs/userguide/tutorials/artists/lineartist.py new file mode 100644 index 00000000000..47dc60be132 --- /dev/null +++ b/docs/userguide/tutorials/artists/lineartist.py @@ -0,0 +1,20 @@ +from math import pi +from compas.geometry import Rotation, Scale +from compas.geometry import Line +from compas.artists import Artist +from compas.colors import Color + +Artist.clear() + +line = Line([0, 0, 0], [3, 0, 0]) +artist = Artist(line) +# artist.color = (1.0, 0.0, 0.0) + +step = pi / 10.0 +rotation = Rotation.from_axis_and_angle([0, 0, 1], angle=step) + +for i in range(11): + artist.draw(color=Color.from_i(i / 10)) + line.transform(rotation) + +Artist.redraw() \ No newline at end of file diff --git a/docs/userguide/tutorials/artists/meshartist.py b/docs/userguide/tutorials/artists/meshartist.py new file mode 100644 index 00000000000..83a3fc8508e --- /dev/null +++ b/docs/userguide/tutorials/artists/meshartist.py @@ -0,0 +1,16 @@ +from compas.datastructures import Mesh +from compas.artists import Artist +from compas.colors import Color + +mesh = Mesh.from_meshgrid(10, 10) + +Artist.clear() + +artist = Artist(mesh) +artist.draw_vertices() +artist.draw_edges() +artist.draw_faces(color={face: Color.pink() for face in mesh.face_sample(size=17)}) +#artist.draw_vertexnormals() +artist.draw_facenormals() + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/networkartist.py b/docs/userguide/tutorials/artists/networkartist.py new file mode 100644 index 00000000000..a53e3429e47 --- /dev/null +++ b/docs/userguide/tutorials/artists/networkartist.py @@ -0,0 +1,21 @@ +from compas.geometry import Pointcloud +from compas.datastructures import Network +from compas.artists import Artist +from compas.colors import Color + +network = Network.from_pointcloud(Pointcloud.from_bounds(8, 5, 3, 53)) + +node = network.node_sample(size=1)[0] +nbrs = network.neighbors(node) +edges = network.connected_edges(node) + +Artist.clear() + +artist = Artist(network) +artist.draw( + nodecolor={n: Color.pink() for n in [node] + nbrs}, + edgecolor={e: Color.pink() for e in edges}, + nodetext='index' +) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/pointartist.py b/docs/userguide/tutorials/artists/pointartist.py new file mode 100644 index 00000000000..28be7d90886 --- /dev/null +++ b/docs/userguide/tutorials/artists/pointartist.py @@ -0,0 +1,17 @@ +import compas_rhino + +from compas.geometry import Point +from compas.artists import Artist +from compas.colors import Color + +compas_rhino.clear() + +point = Point(0, 0, 0) +artist = Artist(point) +# artist.color = (0.0, 1.0, 0.0) + +for i in range(11): + point.x = i + artist.draw(color=Color.from_i(i / 10)) + +compas_rhino.redraw() diff --git a/docs/userguide/tutorials/artists/polygonartist.py b/docs/userguide/tutorials/artists/polygonartist.py new file mode 100644 index 00000000000..81960502493 --- /dev/null +++ b/docs/userguide/tutorials/artists/polygonartist.py @@ -0,0 +1,13 @@ +from compas.geometry import Polygon +from compas.artists import Artist + +Artist.clear() + +polygon = Polygon.from_sides_and_radius_xy(8, 7.0) + +artist = Artist(polygon) +artist.color = (0.0, 1.0, 0.0) + +artist.draw(show_points=True, show_edges=True) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/polyhedronartist.py b/docs/userguide/tutorials/artists/polyhedronartist.py new file mode 100644 index 00000000000..00fab26f62e --- /dev/null +++ b/docs/userguide/tutorials/artists/polyhedronartist.py @@ -0,0 +1,10 @@ +from compas.geometry import Polyhedron +from compas.artists import Artist + +Artist.clear() + +polyhedron = Polyhedron.from_platonicsolid(f=8) +artist = Artist(polyhedron) +artist.draw() + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/polylineartist.py b/docs/userguide/tutorials/artists/polylineartist.py new file mode 100644 index 00000000000..37584c501bf --- /dev/null +++ b/docs/userguide/tutorials/artists/polylineartist.py @@ -0,0 +1,20 @@ +from random import randrange +from compas.geometry import Polyline +from compas.artists import Artist +from compas.colors import Color + +Artist.clear() + +polyline = Polyline([[0, 0, 0]]) + +for i, r in enumerate([randrange(1, 20) for _ in range(10)]): + if i % 2 == 0: + polyline.append([r, polyline.points[-1].y, 0]) + else: + polyline.append([polyline.points[-1].x, r, 0]) + +artist = Artist(polyline) +artist.color = (0.0, 0.0, 1.0) +artist.draw() + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/sphereartist.py b/docs/userguide/tutorials/artists/sphereartist.py new file mode 100644 index 00000000000..551f12051f9 --- /dev/null +++ b/docs/userguide/tutorials/artists/sphereartist.py @@ -0,0 +1,11 @@ +from compas.geometry import Sphere +from compas.artists import Artist + +Artist.clear() + +sphere = Sphere(radius=1.0) + +artist = Artist(sphere) +artist.draw(color=(1.0, 0.0, 0.0)) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/torusartist.py b/docs/userguide/tutorials/artists/torusartist.py new file mode 100644 index 00000000000..2e64114b432 --- /dev/null +++ b/docs/userguide/tutorials/artists/torusartist.py @@ -0,0 +1,11 @@ +from compas.geometry import Torus +from compas.artists import Artist + +Artist.clear() + +torus = Torus(radius_axis=7.0, radius_pipe=2.0) + +artist = Artist(torus) +artist.draw(color=(1.0, 0.0, 0.0)) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/vectorartist.py b/docs/userguide/tutorials/artists/vectorartist.py new file mode 100644 index 00000000000..f1f86d78d2f --- /dev/null +++ b/docs/userguide/tutorials/artists/vectorartist.py @@ -0,0 +1,28 @@ +from math import pi +from compas.geometry import Vector +from compas.geometry import Rotation +from compas.artists import Artist +from compas.colors import Color + +Artist.clear() + +vector = Vector(1, 0, 0) +artist = Artist(vector) +# artist.color = (0.0, 1.0, 0.0) + +# for i in range(11): +# artist.draw( +# color=Color.from_i(i / 10), +# point=[i, 0, 0], +# show_point=True +# ) + +step = pi / 10.0 + +for i in range(11): + artist.draw(color=Color.from_i(i / 10)) + + rotation = Rotation.from_axis_and_angle([0, 0, 1], angle=step) + vector.transform(rotation) + +Artist.redraw() diff --git a/docs/userguide/tutorials/artists/volmeshartist.py b/docs/userguide/tutorials/artists/volmeshartist.py new file mode 100644 index 00000000000..ed42ab05053 --- /dev/null +++ b/docs/userguide/tutorials/artists/volmeshartist.py @@ -0,0 +1,12 @@ +from compas.datastructures import VolMesh +from compas.artists import Artist +from compas.colors import Color + +mesh = VolMesh.from_meshgrid(dx=10, nx=10) + +Artist.clear() + +artist = Artist(mesh) +artist.draw_cells(color={cell: Color.pink() for cell in mesh.cell_sample(size=83)}) + +Artist.redraw() diff --git a/setup.py b/setup.py index 111df667e41..b76ca834fd5 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,6 @@ def read(*names, **kwargs): "compas_rhino", "compas_blender", "compas_ghpython", - "compas_plotters", ], package_dir={"": "src"}, package_data={"compas": ["data/schemas/*.json"]}, diff --git a/src/compas/__init__.py b/src/compas/__init__.py index 1a87032e0eb..6b79cd4aa89 100644 --- a/src/compas/__init__.py +++ b/src/compas/__init__.py @@ -136,6 +136,8 @@ __all_plugins__ = [ "compas.geometry.booleans.booleans_shapely", + "compas.geometry.curves.nurbs_", + "compas.geometry.surfaces.nurbs_", ] diff --git a/src/compas/artists/__init__.py b/src/compas/artists/__init__.py index d36beda5403..48d33bb17f3 100644 --- a/src/compas/artists/__init__.py +++ b/src/compas/artists/__init__.py @@ -5,13 +5,10 @@ from .exceptions import DataArtistNotRegistered from .exceptions import NoArtistContextError from .artist import Artist -from .curveartist import CurveArtist from .meshartist import MeshArtist from .networkartist import NetworkArtist -from .primitiveartist import PrimitiveArtist +from .geometryartist import GeometryArtist from .robotmodelartist import RobotModelArtist -from .shapeartist import ShapeArtist -from .surfaceartist import SurfaceArtist from .volmeshartist import VolMeshArtist from .artist import clear # noqa: F401 @@ -25,12 +22,9 @@ "DataArtistNotRegistered", "NoArtistContextError", "Artist", - "CurveArtist", "MeshArtist", "NetworkArtist", - "PrimitiveArtist", + "GeometryArtist", "RobotModelArtist", - "ShapeArtist", - "SurfaceArtist", "VolMeshArtist", ] diff --git a/src/compas/artists/artist.py b/src/compas/artists/artist.py index 36556331ee5..e796eba97df 100644 --- a/src/compas/artists/artist.py +++ b/src/compas/artists/artist.py @@ -3,7 +3,6 @@ from __future__ import print_function import inspect -from warnings import warn from abc import abstractmethod from collections import defaultdict @@ -13,7 +12,7 @@ from compas.plugins import PluginValidator from compas.plugins import pluggable -from .colordict import DescriptorProtocol +from .descriptors.protocol import DescriptorProtocol @pluggable(category="drawing-utils") @@ -41,18 +40,13 @@ def is_viewer_open(): """ # TODO: implement [without introducing compas_view2 as a dependency..?] - return False - - -def is_plotter_open(): - """Returns True if an instance of the Plotter is available. - - Returns - ------- - bool - - """ - # TODO: implement + # make the viewer app a singleton + # check for the exitence of an instance of the singleton + # if the instance exists, return True + # in this case, the viewer is the current context + # to do this without introducing compas_view2 as a dependency, + # creating the singleton instance should modify a class attribute of the Artist + # (or potentially a module level attribute of compas itself) return False @@ -71,13 +65,13 @@ def _detect_current_context(): """ if is_viewer_open(): return "Viewer" - if is_plotter_open(): - return "Plotter" if compas.is_grasshopper(): return "Grasshopper" if compas.is_rhino(): return "Rhino" - other_contexts = [v for v in Artist.ITEM_ARTIST.keys() if v != "Plotter"] # TODO: remove when Plotter is removed + if compas.is_blender(): + return "Blender" + other_contexts = [v for v in Artist.ITEM_ARTIST.keys()] if other_contexts: return other_contexts[0] raise NoArtistContextError() @@ -87,10 +81,6 @@ def _get_artist_cls(data, **kwargs): # in any case user gets to override the choice context_name = kwargs.get("context") or _detect_current_context() - # TODO: remove when Plotter is removed - if context_name == "Plotter": - warn("The usage of Plotter with COMPAS Artist is deprecated!") - dtype = type(data) cls = None @@ -130,6 +120,7 @@ class Artist(object): """ + # add this to support the descriptor protocol vor Python versions below 3.6 __metaclass__ = DescriptorProtocol __ARTISTS_REGISTERED = False @@ -150,6 +141,10 @@ def __new__(cls, item, **kwargs): PluginValidator.ensure_implementations(cls) return super(Artist, cls).__new__(cls) + def __init__(self, item, **kwargs): + self._item = item + self._transformation = None + @staticmethod def build(item, **kwargs): """Build an artist corresponding to the item type. @@ -235,7 +230,7 @@ def register(item_type, artist_type, context=None): The type of data item. artist_type : :class:`~compas.artists.Artist` The type of the corresponding/compatible artist. - context : Literal['Rhino', 'Grasshopper', 'Blender', 'Plotter'], optional + context : Literal['Viewer', 'Rhino', 'Grasshopper', 'Blender'], optional The visualization context in which the pair should be registered. Returns @@ -245,6 +240,22 @@ def register(item_type, artist_type, context=None): """ Artist.ITEM_ARTIST[context][item_type] = artist_type + @property + def transformation(self): + """The transformation matrix of the artist. + + Returns + ------- + :class:`Transformation` or None + The transformation matrix. + + """ + return self._transformation + + @transformation.setter + def transformation(self, transformation): + self._transformation = transformation + @abstractmethod def draw(self): """The main drawing method.""" diff --git a/src/compas/artists/colordict.py b/src/compas/artists/colordict.py deleted file mode 100644 index f4255565ea7..00000000000 --- a/src/compas/artists/colordict.py +++ /dev/null @@ -1,117 +0,0 @@ -import compas -from collections import defaultdict - -if compas.PY2: - from collections import Mapping -else: - from collections.abc import Mapping -from compas.colors import Color - - -class DescriptorProtocol(type): - """Meta class to provide support for the descriptor protocol in Python versions lower than 3.6""" - - def __init__(cls, name, bases, attrs): - for k, v in iter(attrs.items()): - if hasattr(v, "__set_name__"): - v.__set_name__(cls, k) - - -class ColorDict(object): - """Descriptor for color dictionaries. - - To use this descriptor, some requirements need to be fulfilled. - - The descriptor should be assigned to a class attribute - that has a protected counterpart that will hold the actual dictionary values, - and a corresponding attribute that defines the default value for missing colors. - Both the protected attribute and the default attribute should follow a specific naming convention. - - For example, to create the property ``vertex_color`` on a ``MeshArtist``, - the ``MeshArtist`` should have ``self._vertex_color`` for storing the actual dictionary values, - and ``self.default_vertexcolor`` for storing the replacement value for missing entries in the dict. - - The descriptor will then ensure that all values assigned to ``vertex_color`` will result in the creation - of a ``defaultdict`` that always returns instances of ``compas.colors.Color`` - such that colors can be reliably converted between color spaces as needed, regardless of the input. - - """ - - def __set_name__(self, owner, name): - """Record the name of the attribute this descriptor is assigned to. - The attribute name is then used to identify the corresponding private attribute, and the attribute containing a default value. - - Parameters - ---------- - owner : object - The class owning the attribute. - name : str - The name of the attribute. - - Returns - ------- - None - - Notes - ----- - In Python 3.6+ this method is called automatically. - For earlier versions it needs to be used with a custom metaclass (``DescriptorProtocol``). - - """ - self.public_name = name - self.private_name = "_" + name - self.default_name = "default_" + "".join(name.split("_")) - - def __get__(self, obj, otype=None): - """Get the color dict stored in the private attribute corresponding to the public attribute name of the descriptor. - - Parameters - ---------- - obj : object - The instance owning the instance of the descriptor. - otype : object, optional - The type owning the instance of the descriptor. - - Returns - ------- - defaultdict - A defaultdict with the value stored in the default attribute corresponding to the descriptor as a default value. - - """ - default = getattr(obj, self.default_name) - d = getattr(obj, self.private_name) - if d is not None: - return d - return defaultdict(lambda: default) - - def __set__(self, obj, value): - """Set a new value for the descriptor. - - Parameters - ---------- - obj : object - The owner of the descriptor. - value : dict[Any, :class:`~compas.colors.Color`] | :class:`~compas.colors.Color` - The new value for the descriptor. - This value is stored in the corresponding private attribute in the form of a defaultdict. - - Returns - ------- - None - - """ - if not value: - return - - if isinstance(value, Mapping): - default = getattr(obj, self.default_name) - item_color = defaultdict(lambda: default) - for item in value: - color = Color.coerce(value[item]) - if color: - item_color[item] = color - setattr(obj, self.private_name, item_color) - - else: - color = Color.coerce(value) - setattr(obj, self.private_name, defaultdict(lambda: color)) diff --git a/src/compas/artists/curveartist.py b/src/compas/artists/curveartist.py deleted file mode 100644 index b29fae46f17..00000000000 --- a/src/compas/artists/curveartist.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.colors import Color -from .artist import Artist - - -class CurveArtist(Artist): - """Base class for artists for curves. - - Parameters - ---------- - curve : :class:`~compas.geometry.Curve` - The curve geometry. - color : tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color. - - Attributes - ---------- - curve : :class:`~compas.geometry.Curve` - The geometry of the curve. - color : :class:`~compas.colors.Color` - The color of the curve. - default_color : :class:`~compas.colors.Color` - The default color of the curve. - - See Also - -------- - :class:`compas.artists.SurfaceArtist` - - """ - - default_color = Color.from_hex("#0092D2") - - def __init__(self, curve, color=None, **kwargs): - super(CurveArtist, self).__init__() - self._default_color = None - - self._curve = None - self._color = None - self.curve = curve - self.color = color - - @property - def curve(self): - return self._curve - - @curve.setter - def curve(self, curve): - self._curve = curve - - @property - def color(self): - if not self._color: - self.color = self.default_color - return self._color - - @color.setter - def color(self, color): - self._color = Color.coerce(color) diff --git a/src/compas_blender/ui/forms/__init__.py b/src/compas/artists/descriptors/__init__.py similarity index 100% rename from src/compas_blender/ui/forms/__init__.py rename to src/compas/artists/descriptors/__init__.py diff --git a/src/compas/artists/descriptors/attribute.py b/src/compas/artists/descriptors/attribute.py new file mode 100644 index 00000000000..581bea52586 --- /dev/null +++ b/src/compas/artists/descriptors/attribute.py @@ -0,0 +1,30 @@ +class Attribute(object): + """Descriptor for text dictionaries.""" + + def __init__(self, default=None, **kwargs): + super(Attribute, self).__init__(**kwargs) + self.default = default + + def __set_name__(self, owner, name): + """Record the name of the attribute this descriptor is assigned to. + The attribute name is then used to identify the corresponding private attribute. + + Parameters + ---------- + owner : object + The class owning the attribute. + name : str + The name of the attribute. + + Returns + ------- + None + + Notes + ----- + In Python 3.6+ this method is called automatically. + For earlier versions it needs to be used with a custom metaclass (``DescriptorProtocol``). + + """ + self.name = name + self.private_name = "_" + name diff --git a/src/compas/artists/descriptors/color.py b/src/compas/artists/descriptors/color.py new file mode 100644 index 00000000000..0ca8eefbbaf --- /dev/null +++ b/src/compas/artists/descriptors/color.py @@ -0,0 +1,90 @@ +from compas.colors import Color + + +class ColorAttribute(object): + """A descriptor for color attributes. + + Parameters + ---------- + default : :class:`~compas.colors.Color`, optional + The default value of the attribute. + Default is ``None``. + + """ + + def __init__(self, default, **kwargs): + super(ColorAttribute, self).__init__(**kwargs) + self.default = Color.coerce(default) + + def __set_name__(self, owner, name): + """Record the name of the attribute this descriptor is assigned to. + The attribute name is then used to identify the corresponding private attribute. + + Parameters + ---------- + owner : object + The class owning the attribute. + name : str + The name of the attribute. + + Returns + ------- + None + + Notes + ----- + In Python 3.6+ this method is called automatically. + For earlier versions it needs to be used with a custom metaclass (``DescriptorProtocol``). + + """ + self.name = name + self.private_name = "_" + name + + def __get__(self, obj, otype=None): + """Get the color stored in the private attribute corresponding to the public attribute name of the descriptor. + + Parameters + ---------- + obj : object + The instance owning the instance of the descriptor. + otype : object, optional + The type owning the instance of the descriptor. + + Returns + ------- + :class:`~compas.colors.Color` + The color stored in the private attribute corresponding to the public attribute name of the descriptor. + If the private attribute does not exist, the default value of the descriptor is returned. + + """ + if not hasattr(obj, self.private_name): + setattr(obj, self.private_name, None) + + color = getattr(obj, self.private_name) + + if color is None: + color = Color.coerce(self.default) + setattr(obj, self.private_name, color) + + return color + + def __set__(self, obj, value): + """Set a new value for the descriptor. + + Parameters + ---------- + obj : object + The owner of the descriptor. + value : :class:`~compas.colors.Color` + The new value for the descriptor. + This value is stored in the corresponding private attribute. + + Returns + ------- + None + + """ + if not value: + return + + setattr(obj, self.private_name, Color.coerce(value)) diff --git a/src/compas/artists/descriptors/colordict.py b/src/compas/artists/descriptors/colordict.py new file mode 100644 index 00000000000..d338449fd6c --- /dev/null +++ b/src/compas/artists/descriptors/colordict.py @@ -0,0 +1,95 @@ +import compas + +if compas.PY2: + from collections import Mapping +else: + from collections.abc import Mapping +from compas.colors.colordict import ColorDict + + +class ColorDictAttribute(object): + """Descriptor for color dictionaries.""" + + def __init__(self, default=None, **kwargs): + super(ColorDictAttribute, self).__init__(**kwargs) + self.default = default + + def __set_name__(self, owner, name): + """Record the name of the attribute this descriptor is assigned to. + The attribute name is then used to identify the corresponding private attribute. + + Parameters + ---------- + owner : object + The class owning the attribute. + name : str + The name of the attribute. + + Returns + ------- + None + + Notes + ----- + In Python 3.6+ this method is called automatically. + For earlier versions it needs to be used with a custom metaclass (``DescriptorProtocol``). + + """ + self.name = name + self.private_name = "_" + name + + def __get__(self, obj, otype=None): + """Get the color dict stored in the private attribute corresponding to the public attribute name of the descriptor. + + Parameters + ---------- + obj : object + The instance owning the instance of the descriptor. + otype : object, optional + The type owning the instance of the descriptor. + + Returns + ------- + defaultdict + A defaultdict with the value stored in the default attribute corresponding to the descriptor as a default value. + + """ + if not hasattr(obj, self.private_name): + setattr(obj, self.private_name, None) + + colordict = getattr(obj, self.private_name) + + if colordict is None: + colordict = ColorDict(default=self.default) + setattr(obj, self.private_name, colordict) + + return colordict + + def __set__(self, obj, value): + """Set a new value for the descriptor. + + Parameters + ---------- + obj : object + The owner of the descriptor. + value : dict[Any, :class:`~compas.colors.Color`] | :class:`~compas.colors.Color` + The new value for the descriptor. + This value is stored in the corresponding private attribute in the form of a defaultdict. + + Returns + ------- + None + + """ + if not value: + return + + colordict = getattr(obj, self.name) + + if isinstance(value, Mapping): + colordict.clear() + colordict.update(value) + + else: + colordict.clear() + colordict.default = value diff --git a/src/compas/artists/descriptors/protocol.py b/src/compas/artists/descriptors/protocol.py new file mode 100644 index 00000000000..b7a20ad4283 --- /dev/null +++ b/src/compas/artists/descriptors/protocol.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + + +class DescriptorProtocol(type): + """Meta class to provide support for the descriptor protocol in Python versions lower than 3.6""" + + def __init__(cls, name, bases, attrs): + for key, value in iter(attrs.items()): + if hasattr(value, "__set_name__"): + value.__set_name__(cls, key) diff --git a/src/compas/artists/geometryartist.py b/src/compas/artists/geometryartist.py new file mode 100644 index 00000000000..737ed654314 --- /dev/null +++ b/src/compas/artists/geometryartist.py @@ -0,0 +1,35 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from .artist import Artist +from .descriptors.color import ColorAttribute + + +class GeometryArtist(Artist): + """Base class for artists for geometry objects. + + Parameters + ---------- + geometry : :class:`~compas.geometry.Geometry` + The geometry of the geometry. + + Attributes + ---------- + geometry : :class:`~compas.geometry.Geometry` + The geometry object associated with the artist. + color : :class:`~compas.colors.Color` + The color of the object. + + See Also + -------- + :class:`compas.artists.CurveArtist` + :class:`compas.artists.SurfaceArtist` + + """ + + color = ColorAttribute(default=None) + + def __init__(self, geometry, **kwargs): + super(GeometryArtist, self).__init__(item=geometry, **kwargs) + self.geometry = geometry diff --git a/src/compas/artists/meshartist.py b/src/compas/artists/meshartist.py index d85960025ef..da429f08f5c 100644 --- a/src/compas/artists/meshartist.py +++ b/src/compas/artists/meshartist.py @@ -4,9 +4,10 @@ from abc import abstractmethod -from compas.colors import Color +from compas.geometry import transform_points from .artist import Artist -from .colordict import ColorDict +from .descriptors.color import ColorAttribute +from .descriptors.colordict import ColorDictAttribute class MeshArtist(Artist): @@ -16,76 +17,22 @@ class MeshArtist(Artist): ---------- mesh : :class:`~compas.datastructures.Mesh` A COMPAS mesh. - vertices : list[int], optional - Selection of vertices to draw. - edges : list[tuple[int, int]], optional - Selection of edges to draw. - faces : list[int], optional - Selection of faces to draw. - vertexcolor : tuple[float, float, float] | dict[int, tuple[float, float, float]], optional - Color of the vertices. - Default color is :attr:`MeshArtists.default_vertexcolor`. - edgecolor : tuple[float, float, float] | dict[tuple[int, int], tuple[float, float, float]], optional - Color of the edges. - Default color is :attr:`MeshArtists.default_edgecolor`. - facecolor : tuple[float, float, float] | dict[int, tuple[float, float, float]], optional - Color of the faces. - Default color is :attr:`MeshArtists.default_facecolor`. Attributes ---------- mesh : :class:`~compas.datastructures.Mesh` The mesh data structure. - vertices : list[int] - The selection of vertices that should be included in the drawing. - Defaults to all vertices. - edges : list[tuple[int, int]] - The selection of edges that should be included in the drawing. - Defaults to all edges. - faces : list[int] - The selection of faces that should be included in the drawing. - Defaults to all faces. - color : :class:`~compas.colors.Color` - The base RGB color of the mesh. vertex_xyz : dict[int, list[float]] View coordinates of the vertices. Defaults to the real coordinates. - vertex_color : dict[int, :class:`~compas.colors.Color`] + color : :class:`~compas.colors.Color` + The base RGB color of the mesh. + vertex_color : :class:`~compas.colors.ColorDict`] Vertex colors. - Missing vertices get the default vertex color :attr:`default_vertexcolor`. - default_vertexcolor : :class:`~compas.colors.Color` - The default color of the vertices of the mesh. - edge_color : dict[tuple[int, int], :class:`~compas.colors.Color`] + edge_color : :class:`~compas.colors.ColorDict` Edge colors. - Missing edges get the default edge color :attr:`default_edgecolor`. - default_edgecolor : :class:`~compas.colors.Color` - The default color of the edges of the mesh. - face_color : dict[int, :class:`~compas.colors.Color`] + face_color : :class:`~compas.colors.ColorDict` Face colors. - Missing faces get the default face color :attr:`default_facecolor`. - default_facecolor : :class:`~compas.colors.Color` - The default color of the faces of the mesh. - vertex_text : dict[int, str] - Vertex labels. - Defaults to the vertex identifiers. - edge_text : dict[tuple[int, int], str] - Edge labels. - Defaults to the edge identifiers. - face_text : dict[int, str] - Face labels. - Defaults to the face identifiers. - vertex_size : dict[int, float] - Vertex sizes. - Defaults to 1. - Visualization of vertices with variable size is not available for all visualization contexts. - default_vertexsize : float - The default size of the vertices of the mesh. - edge_width : dict[tuple[int, int], float] - Edge widths. - Defaults to 1. - Visualization of edges with variable width is not available for all visualization contexts. - default_edgewidth : float - The default width of the edges of the mesh. See Also -------- @@ -94,61 +41,18 @@ class MeshArtist(Artist): """ - color = Color.from_hex("#0092D2").lightened(50) - - default_vertexcolor = Color.from_hex("#0092D2") - default_edgecolor = Color.from_hex("#0092D2") - default_facecolor = Color.from_hex("#0092D2").lightened(50) - - vertex_color = ColorDict() - edge_color = ColorDict() - face_color = ColorDict() - - default_vertexsize = 5 - default_edgewidth = 1.0 - - def __init__( - self, mesh, vertices=None, edges=None, faces=None, vertexcolor=None, edgecolor=None, facecolor=None, **kwargs - ): - super(MeshArtist, self).__init__() + color = ColorAttribute(default=None) - self._default_vertexcolor = None - self._default_edgecolor = None - self._default_facecolor = None + vertex_color = ColorDictAttribute(default=None) + edge_color = ColorDictAttribute(default=None) + face_color = ColorDictAttribute(default=None) + def __init__(self, mesh, **kwargs): + super(MeshArtist, self).__init__(item=mesh, **kwargs) self._mesh = None - self._vertices = None - self._edges = None - self._faces = None - self._color = None self._vertex_xyz = None - self._vertex_color = None - self._vertex_text = None - self._vertex_size = None - self._edge_color = None - self._edge_text = None - self._edge_width = None - self._face_color = None - self._face_text = None - - self._vertexcollection = None - self._edgecollection = None - self._facecollection = None - self._vertexnormalcollection = None - self._facenormalcollection = None - self._vertexlabelcollection = None - self._edgelabelcollection = None - self._facelabelcollection = None - self.mesh = mesh - self.vertices = vertices - self.edges = edges - self.faces = faces - self.vertex_color = vertexcolor - self.edge_color = edgecolor - self.face_color = facecolor - @property def mesh(self): return self._mesh @@ -156,119 +60,31 @@ def mesh(self): @mesh.setter def mesh(self, mesh): self._mesh = mesh + self._transformation = None 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 + def transformation(self): + return self._transformation - @faces.setter - def faces(self, faces): - self._faces = faces + @transformation.setter + def transformation(self, transformation): + self._vertex_xyz = None + self._transformation = transformation @property def vertex_xyz(self): if self._vertex_xyz is None: - return {vertex: self.mesh.vertex_attributes(vertex, "xyz") for vertex in self.mesh.vertices()} + points = self.mesh.vertices_attributes("xyz") # type: ignore + if self.transformation: + points = transform_points(points, self.transformation) + self._vertex_xyz = dict(zip(self.mesh.vertices(), points)) # type: ignore return self._vertex_xyz @vertex_xyz.setter def vertex_xyz(self, vertex_xyz): self._vertex_xyz = vertex_xyz - @property - def vertex_text(self): - if self._vertex_text is None: - self._vertex_text = {vertex: str(vertex) for vertex in self.mesh.vertices()} - return self._vertex_text - - @vertex_text.setter - def vertex_text(self, text): - if text == "key": - self._vertex_text = {vertex: str(vertex) for vertex in self.mesh.vertices()} - elif text == "index": - self._vertex_text = {vertex: str(index) for index, vertex in enumerate(self.mesh.vertices())} - elif isinstance(text, dict): - self._vertex_text = text - - @property - def vertex_size(self): - if not self._vertex_size: - self._vertex_size = {vertex: self.default_vertexsize for vertex in self.mesh.vertices()} - return self._vertex_size - - @vertex_size.setter - def vertex_size(self, vertexsize): - if isinstance(vertexsize, dict): - self._vertex_size = vertexsize - elif isinstance(vertexsize, (int, float)): - self._vertex_size = {vertex: vertexsize for vertex in self.mesh.vertices()} - - @property - def edge_text(self): - if self._edge_text is None: - self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.mesh.edges()} - return self._edge_text - - @edge_text.setter - def edge_text(self, text): - if text == "key": - self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.mesh.edges()} - elif text == "index": - self._edge_text = {edge: str(index) for index, edge in enumerate(self.mesh.edges())} - elif isinstance(text, dict): - self._edge_text = text - - @property - def edge_width(self): - if not self._edge_width: - self._edge_width = {edge: self.default_edgewidth for edge in self.mesh.edges()} - return self._edge_width - - @edge_width.setter - def edge_width(self, edgewidth): - if isinstance(edgewidth, dict): - self._edge_width = edgewidth - elif isinstance(edgewidth, (int, float)): - self._edge_width = {edge: edgewidth for edge in self.mesh.edges()} - - @property - def face_text(self): - if self._face_text is None: - self._face_text = {face: str(face) for face in self.mesh.faces()} - return self._face_text - - @face_text.setter - def face_text(self, text): - if text == "key": - self._face_text = {face: str(face) for face in self.mesh.faces()} - elif text == "index": - self._face_text = {face: str(index) for index, face in enumerate(self.mesh.faces())} - elif isinstance(text, dict): - self._face_text = text - @abstractmethod def draw_vertices(self, vertices=None, color=None, text=None): """Draw the vertices of the mesh. @@ -358,7 +174,6 @@ def draw_mesh(self, *args, **kwargs): """ return self.draw(*args, **kwargs) - @abstractmethod def clear(self): """Clear all components of the mesh. @@ -369,7 +184,6 @@ def clear(self): """ raise NotImplementedError - @abstractmethod def clear_vertices(self): """Clear the vertices of the mesh. @@ -380,7 +194,6 @@ def clear_vertices(self): """ raise NotImplementedError - @abstractmethod def clear_edges(self): """Clear the edges of the mesh. @@ -391,7 +204,6 @@ def clear_edges(self): """ raise NotImplementedError - @abstractmethod def clear_faces(self): """Clear the faces of the mesh. diff --git a/src/compas/artists/networkartist.py b/src/compas/artists/networkartist.py index dc7c399e857..cb1890e019a 100644 --- a/src/compas/artists/networkartist.py +++ b/src/compas/artists/networkartist.py @@ -5,8 +5,9 @@ from abc import abstractmethod from compas.colors import Color +from compas.geometry import transform_points from .artist import Artist -from .colordict import ColorDict +from .descriptors.colordict import ColorDictAttribute class NetworkArtist(Artist): @@ -16,54 +17,18 @@ class NetworkArtist(Artist): ---------- network : :class:`~compas.datastructures.Network` A COMPAS network. - nodes : list[hashable], optional - A list of node identifiers. - Default is None, in which case all nodes are drawn. - edges : list[tuple[hashable, hashable]], 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 : :class:`~compas.colors.Color` | dict[hashable, :class:`~compas.colors.Color`], optional - The color specification for the nodes. - edgecolor : :class:`~compas.colors.Color` | dict[tuple[hashable, hashable]], :class:`~compas.colors.Color`], optional - The color specification for the edges. Attributes ---------- network : :class:`~compas.datastructures.Network` The COMPAS network associated with the artist. - nodes : list[hashable] - The list of nodes to draw. - Defaults to all nodes. - edges : list[tuple[hashable, hashable]] - The list of edges to draw. - Default is a list of all edges of the network. node_xyz : dict[hashable, list[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[hashable, :class:`~compas.colors.Color`] + node_color : :class:`~compas.colors.ColorDict` Mapping between nodes and RGB color values. - Missing nodes get the default node color :attr:`default_nodecolor`. - default_nodecolor : :class:`~compas.colors.Color` - The default color for nodes that do not have a specified color. - edge_color : dict[tuple[hashable, hashable], :class:`~compas.colors.Color`] + edge_color : :class:`~compas.colors.ColorDict` Mapping between edges and colors. - Missing edges get the default edge color :attr:`default_edgecolor`. - default_edgecolor : :class:`~compas.colors.Color` - The default color for edges that do not have a specified color. - node_text : dict[hashable, str] - Mapping between nodes and text labels. - edge_text : dict[tuple[hashable, hashable], str] - Mapping between edges and text labels. - node_size : dict[hashable, float] - Mapping between nodes and sizes. - Missing nodes get assigned the default node size :attr:`default_nodesize`. - default_nodesize : float - The default size for nodes that do not have a specified size. - edge_width : dict[tuple[hashable, hashable], float] - Mapping between edges and line widths. - Missing edges get assigned the default edge width :attr:`default_edgewidth`. - default_edgewidth : float - The default width for edges that do not have a specified width. See Also -------- @@ -72,43 +37,15 @@ class NetworkArtist(Artist): """ - default_nodecolor = Color.from_hex("#0092D2") - default_edgecolor = Color.from_hex("#0092D2") - - node_color = ColorDict() - edge_color = ColorDict() - - default_nodesize = 5 - default_edgewidth = 1.0 - - def __init__(self, network, nodes=None, edges=None, nodecolor=None, edgecolor=None, **kwargs): - super(NetworkArtist, self).__init__() - - self._default_nodecolor = None - self._default_edgecolor = None + node_color = ColorDictAttribute(default=Color.white()) + edge_color = ColorDictAttribute(default=Color.black()) + def __init__(self, network, **kwargs): + super(NetworkArtist, self).__init__(**kwargs) self._network = None - self._nodes = None - self._edges = None self._node_xyz = None - self._node_color = None - self._edge_color = None - self._node_text = None - self._edge_text = None - self._edge_width = None - - self._nodecollection = None - self._edgecollection = None - self._nodelabelcollection = None - self._edgelabelcollection = None - self.network = network - self.nodes = nodes - self.edges = edges - self.node_color = nodecolor - self.edge_color = edgecolor - @property def network(self): return self._network @@ -116,94 +53,31 @@ def network(self): @network.setter def network(self, network): self._network = network + self._transformation = None 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 + def transformation(self): + return self._transformation - @edges.setter - def edges(self, edges): - self._edges = edges + @transformation.setter + def transformation(self, transformation): + self._node_xyz = None + self._transformation = transformation @property def node_xyz(self): - if not self._node_xyz: - return {node: self.network.node_attributes(node, "xyz") for node in self.network.nodes()} + if self._node_xyz is None: + points = self.network.nodes_attributes("xyz") # type: ignore + if self.transformation: + points = transform_points(points, self.transformation) + self._node_xyz = dict(zip(self.network.nodes(), points)) # type: ignore return self._node_xyz @node_xyz.setter def node_xyz(self, node_xyz): self._node_xyz = node_xyz - @property - def node_size(self): - if not self._node_size: - self._node_size = {node: self.default_nodesize for node in self.network.nodes()} - 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.nodes()} - - @property - def node_text(self): - if not self._node_text: - self._node_text = {node: str(node) for node in self.network.nodes()} - return self._node_text - - @node_text.setter - def node_text(self, text): - if text == "key": - self._node_text = {node: str(node) for node in self.network.nodes()} - elif text == "index": - self._node_text = {node: str(index) for index, node in enumerate(self.network.nodes())} - elif isinstance(text, dict): - self._node_text = text - - @property - def edge_text(self): - if not self._edge_text: - self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.network.edges()} - return self._edge_text - - @edge_text.setter - def edge_text(self, text): - if text == "key": - self._edge_text = {edge: "{}-{}".format(*edge) for edge in self.network.edges()} - elif text == "index": - self._edge_text = {edge: str(index) for index, edge in enumerate(self.network.edges())} - elif isinstance(text, dict): - self._edge_text = text - - @property - def edge_width(self): - if not self._edge_width: - self._edge_width = {edge: self.default_edgewidth for edge in self.network.edges()} - return self._edge_width - - @edge_width.setter - def edge_width(self, edgewidth): - if isinstance(edgewidth, dict): - self._edge_width = edgewidth - elif isinstance(edgewidth, (int, float)): - self._edge_width = {edge: edgewidth for edge in self.network.edges()} - @abstractmethod def draw_nodes(self, nodes=None, color=None, text=None): """Draw the nodes of the network. diff --git a/src/compas/artists/primitiveartist.py b/src/compas/artists/primitiveartist.py deleted file mode 100644 index 04edc87df94..00000000000 --- a/src/compas/artists/primitiveartist.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.colors import Color -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[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB components of the base color of the primitive. - - Attributes - ---------- - primitive : :class:`~compas.geometry.Primitive` - The geometric primitive associated with the artist. - color : :class:`~compas.colors.Color` - The color of the object. - default_color : :class:`~compas.colors.Color` - The default rgb color value of the primitive. - - See Also - -------- - :class:`compas.artists.ShapeArtist` - - """ - - default_color = Color.from_hex("#0092D2") - - def __init__(self, primitive, color=None, **kwargs): - super(PrimitiveArtist, self).__init__() - self._default_color = None - - 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, value): - self._color = Color.coerce(value) diff --git a/src/compas/artists/robotmodelartist.py b/src/compas/artists/robotmodelartist.py index 1206f207f37..4b9ea5afed4 100644 --- a/src/compas/artists/robotmodelartist.py +++ b/src/compas/artists/robotmodelartist.py @@ -70,8 +70,8 @@ class RobotModelArtist(AbstractRobotModelArtist, Artist): """ - def __init__(self, model): - super(RobotModelArtist, self).__init__() + def __init__(self, model, **kwargs): + super(RobotModelArtist, self).__init__(item=model, **kwargs) self.model = model self.create() self.scale_factor = 1.0 diff --git a/src/compas/artists/shapeartist.py b/src/compas/artists/shapeartist.py deleted file mode 100644 index c561e0ce357..00000000000 --- a/src/compas/artists/shapeartist.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.colors import Color -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[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color. - - Attributes - ---------- - shape : :class:`~compas.geometry.Shape` - The geometry of the shape. - color : :class:`~compas.colors.Color` - The color of the shape. - default_color : :class:`~compas.colors.Color` - The default color of the shape. - u : int - The resolution in the U direction of the discrete shape representation. - v : int - The resolution in the V direction of the discrete shape representation. - - See Also - -------- - :class:`compas.artists.PrimitiveArtist` - - """ - - default_color = Color.from_hex("#0092D2") - - def __init__(self, shape, color=None, **kwargs): - super(ShapeArtist, self).__init__() - self._default_color = None - - self._u = None - self._v = None - self._shape = None - self._color = None - - self.shape = shape - self.color = color - self.u = kwargs.get("u") - self.v = kwargs.get("v") - - @property - def shape(self): - return self._shape - - @shape.setter - def shape(self, shape): - self._shape = shape - - @property - def color(self): - if not self._color: - self.color = self.default_color - return self._color - - @color.setter - def color(self, value): - self._color = Color.coerce(value) - - @property - def u(self): - if not self._u: - self._u = 16 - return self._u - - @u.setter - def u(self, u): - if u and u > 3: - self._u = u - - @property - def v(self): - if not self._v: - self._v = 16 - return self._v - - @v.setter - def v(self, v): - if v and v > 3: - self._v = v diff --git a/src/compas/artists/surfaceartist.py b/src/compas/artists/surfaceartist.py deleted file mode 100644 index 3218c52f83b..00000000000 --- a/src/compas/artists/surfaceartist.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.colors import Color -from .artist import Artist - - -class SurfaceArtist(Artist): - """Base class for artists for surfaces. - - Parameters - ---------- - surface : :class:`~compas.geometry.Surface` - The surface geometry. - color : tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color. - - Attributes - ---------- - surface : :class:`~compas.geometry.Surface` - The geometry of the surface. - color : :class:`~compas.colors.Color` - The color of the surface. - default_color : :class:`~compas.colors.Color` - The default color of the surface. - - See Also - -------- - :class:`compas.artists.CurveArtist` - - """ - - default_color = Color.from_hex("#0092D2") - - def __init__(self, surface, color=None, **kwargs): - super(SurfaceArtist, self).__init__() - self._default_color = None - - self._surface = None - self._color = None - self.surface = surface - self.color = color - - @property - def surface(self): - return self._surface - - @surface.setter - def surface(self, surface): - self._surface = surface - - @property - def color(self): - if not self._color: - self.color = self.default_color - return self._color - - @color.setter - def color(self, value): - self._color = Color.coerce(value) diff --git a/src/compas/artists/volmeshartist.py b/src/compas/artists/volmeshartist.py index adef25c3280..72f02dab346 100644 --- a/src/compas/artists/volmeshartist.py +++ b/src/compas/artists/volmeshartist.py @@ -5,8 +5,10 @@ from abc import abstractmethod from compas.colors import Color +from compas.geometry import transform_points from .artist import Artist -from .colordict import ColorDict +from .descriptors.color import ColorAttribute +from .descriptors.colordict import ColorDictAttribute class VolMeshArtist(Artist): @@ -21,49 +23,21 @@ class VolMeshArtist(Artist): ---------- volmesh : :class:`~compas.datastructures.VolMesh` The COMPAS volmesh associated with the artist. - vertices : list[int] - The list of vertices to draw. - Default is a list of all vertices of the volmesh. - edges : list[tuple[int, int]] - The list of edges to draw. - Default is a list of all edges of the volmesh. - faces : list[int] - The list of faces to draw. - Default is a list of all faces of the volmesh. - cells : list[int] - The list of cells to draw. - Default is a list of all cells of the volmesh. vertex_xyz : dict[int, list[float]] The view coordinates of the vertices. By default, the actual vertex coordinates are used. vertex_color : dict[int, :class:`~compas.colors.Color`] Mapping between vertices and colors. Missing vertices get the default vertex color: :attr:`default_vertexcolor`. - default_vertexcolor : :class:`~compas.colors.Color` - The default color of the vertices of the mesh that don't have a specified color. edge_color : dict[tuple[int, int], :class:`~compas.colors.Color`] Mapping between edges and colors. Missing edges get the default edge color: :attr:`default_edgecolor`. - default_edgecolor : :class:`~compas.colors.Color` - The default color of the edges of the mesh that don't have a specified color. face_color : dict[int, :class:`~compas.colors.Color`] Mapping between faces and colors. Missing faces get the default face color: :attr:`default_facecolor`. - default_facecolor : :class:`~compas.colors.Color` - The default color of the faces of the mesh that don't have a specified color. cell_color : dict[int, :class:`~compas.colors.Color`] Mapping between cells and colors. Missing cells get the default cell color: :attr:`default_facecolor`. - default_cellcolor : :class:`~compas.colors.Color` - The default color of the cells of the mesh that don't have a specified color. - vertex_text : dict[int, str] - Mapping between vertices and text labels. - edge_text : dict[tuple[int, int], str] - Mapping between edges and text labels. - face_text : dict[int, str] - Mapping between faces and text lables. - cell_text : dict[int, str] - Mapping between cells and text lables. See Also -------- @@ -72,75 +46,19 @@ class VolMeshArtist(Artist): """ - color = Color.from_hex("#0092D2").lightened(50) - - default_vertexcolor = Color.from_hex("#0092D2") - default_edgecolor = Color.from_hex("#0092D2") - default_facecolor = Color.from_hex("#0092D2").lightened(50) - default_cellcolor = Color.from_hex("#0092D2").lightened(50) - - vertex_color = ColorDict() - edge_color = ColorDict() - face_color = ColorDict() - cell_color = ColorDict() - - def __init__( - self, - volmesh, - vertices=None, - edges=None, - faces=None, - cells=None, - vertexcolor=None, - edgecolor=None, - facecolor=None, - cellcolor=None, - **kwargs - ): - super(VolMeshArtist, self).__init__() - - self._default_vertexcolor = None - self._default_edgecolor = None - self._default_facecolor = None - self._default_cellcolor = None + color = ColorAttribute(default=Color.grey().lightened(50)) + vertex_color = ColorDictAttribute(default=Color.white()) + edge_color = ColorDictAttribute(default=Color.black()) + face_color = ColorDictAttribute(default=Color.grey().lightened(50)) + cell_color = ColorDictAttribute(default=Color.grey()) + + def __init__(self, volmesh, **kwargs): + super(VolMeshArtist, self).__init__(item=volmesh, **kwargs) self._volmesh = None - self._vertices = None - self._edges = None - self._faces = None - self._cells = None self._vertex_xyz = None - self._vertex_color = None - self._edge_color = None - self._face_color = None - self._cell_color = None - self._vertex_text = None - self._edge_text = None - self._face_text = None - self._cell_text = None - - self._vertexcollection = None - self._edgecollection = None - self._facecollection = None - self._cellcollection = None - self._vertexnormalcollection = None - self._facenormalcollection = None - self._vertexlabelcollection = None - self._edgelabelcollection = None - self._facelabelcollection = None - self._celllabelcollection = None - self.volmesh = volmesh - self.vertices = vertices - self.edges = edges - self.faces = faces - self.cells = cells - self.vertex_color = vertexcolor - self.edge_color = edgecolor - self.face_color = facecolor - self.cell_color = cellcolor - @property def volmesh(self): return self._volmesh @@ -148,120 +66,31 @@ def volmesh(self): @volmesh.setter def volmesh(self, volmesh): self._volmesh = volmesh + self._transformation = None 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 + def transformation(self): + return self._transformation - @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 + @transformation.setter + def transformation(self, transformation): + self._vertex_xyz = None + self._transformation = transformation @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() - } + if self._vertex_xyz is None: + points = self.volmesh.vertices_attributes("xyz") # type: ignore + if self.transformation: + points = transform_points(points, self.transformation) + self._vertex_xyz = dict(zip(self.volmesh.vertices(), points)) # type: ignore return self._vertex_xyz @vertex_xyz.setter def vertex_xyz(self, vertex_xyz): self._vertex_xyz = vertex_xyz - @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. diff --git a/src/compas/colors/color.py b/src/compas/colors/color.py index 9a8b59558e7..aa29dd139f8 100644 --- a/src/compas/colors/color.py +++ b/src/compas/colors/color.py @@ -147,33 +147,6 @@ def __iter__(self): def __eq__(self, other): return all(a == b for a, b in zip(self, other)) - # -------------------------------------------------------------------------- - # Descriptor - # -------------------------------------------------------------------------- - - def __set_name__(self, owner, name): - self.public_name = name - self.private_name = "_" + name - - def __get__(self, obj, otype=None): - return getattr(obj, self.private_name, None) or self - - def __set__(self, obj, value): - if not obj: - return - - if not value: - return - - if Color.is_rgb255(value): - value = Color.from_rgb255(value[0], value[1], value[2]) - elif Color.is_hex(value): - value = Color.from_hex(value) - else: - value = Color(value[0], value[1], value[2]) - - setattr(obj, self.private_name, value) - # -------------------------------------------------------------------------- # Data # -------------------------------------------------------------------------- diff --git a/src/compas/colors/colordict.py b/src/compas/colors/colordict.py new file mode 100644 index 00000000000..2915b1083fd --- /dev/null +++ b/src/compas/colors/colordict.py @@ -0,0 +1,95 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.data import Data +from .color import Color + + +class ColorDict(Data): + """Class representing a dictionary of colors. + + Attributes + ---------- + default : :class:`~compas.colors.Color` + The default color to use if the requested key is not in the dictionary. + + """ + + def __init__(self, default, name=None): + super(ColorDict, self).__init__(name=name) + self._default = None + self.default = default + self._dict = {} + + @property + def default(self): + if not self._default: + self._default = Color(0, 0, 0) + return self._default + + @default.setter + def default(self, default): + if default and not isinstance(default, Color): + default = Color.coerce(default) + self._default = default + + def __getitem__(self, key): + return self._dict.get(key, self.default) + + def __setitem__(self, key, value): + self._dict[key] = Color.coerce(value) + + def __delitem__(self, key): + del self._dict[key] + + def __iter__(self): + return iter(self._dict) + + def __len__(self): + return len(self._dict) + + def __contains__(self, key): + return key in self._dict + + @property + def data(self): + return {"default": self.default.data, "dict": self._dict} + + def items(self): + return self._dict.items() + + def keys(self): + return self._dict.keys() + + def values(self): + return self._dict.values() + + def get(self, key, default=None): + return self._dict.get(key, default or self.default) + + def clear(self): + """Clear the previously stored items. + + Returns + ------- + None + + """ + self._dict = {} + + def update(self, other): + """Update the dictionary with the items from another dictionary. + + Parameters + ---------- + other : dict or :class:`~compas.artists.ColorDict` + The other dictionary. + + Returns + ------- + None + + """ + for key, value in other.items(): + self[key] = value diff --git a/src/compas/colors/colormap.py b/src/compas/colors/colormap.py index 5f9306848e4..081f412cdc6 100644 --- a/src/compas/colors/colormap.py +++ b/src/compas/colors/colormap.py @@ -57,7 +57,7 @@ class ColorMap(object): """ def __init__(self, colors): - self._colors = None + self._colors = [] self.colors = colors # -------------------------------------------------------------------------- @@ -282,7 +282,7 @@ def from_three_colors(cls, c1, c2, c3): return cls(colors) @classmethod - def from_rgb(cls): + def from_rgb(cls, n=256): """Construct a color map from the complete rgb color space. Returns @@ -291,7 +291,7 @@ def from_rgb(cls): """ colors = [] - for i in linspace(0, 1.0, 256): + for i in linspace(0, 1.0, n): colors.append(Color.from_i(i)) return cls(colors) @@ -299,36 +299,36 @@ def from_rgb(cls): # methods # -------------------------------------------------------------------------- - def plot(self): - """Visualize the current map with the plotter. - - Returns - ------- - None - - """ - from compas_plotters.plotter import Plotter - from compas.geometry import Pointcloud - from compas.geometry import Plane, Circle, Polygon - - plotter = Plotter(figsize=(16, 12)) - w = 16 - h = 10 - n = len(self.colors) - d = w / n - cloud = Pointcloud.from_bounds(w, h, 0, n) - white = Color.white() - for i, color in enumerate(self.colors): - c = Circle(Plane(cloud[i], [0, 0, 1]), 0.1) - p = Polygon( - [ - [i * d, -2, 0], - [(i + 1) * d, -2, 0], - [(i + 1) * d, -1, 0], - [i * d, -1, 0], - ] - ) - plotter.add(c, facecolor=color, edgecolor=white, linewidth=0.5) - plotter.add(p, facecolor=color, edgecolor=color) - plotter.zoom_extents() - plotter.show() + # def plot(self): + # """Visualize the current map with the plotter. + + # Returns + # ------- + # None + + # """ + # from compas_plotters.plotter import Plotter + # from compas.geometry import Pointcloud + # from compas.geometry import Plane, Circle, Polygon + + # plotter = Plotter(figsize=(16, 12)) + # w = 16 + # h = 10 + # n = len(self.colors) + # d = w / n + # cloud = Pointcloud.from_bounds(w, h, 0, n) + # white = Color.white() + # for i, color in enumerate(self.colors): + # c = Circle(Plane(cloud[i], [0, 0, 1]), 0.1) + # p = Polygon( + # [ + # [i * d, -2, 0], + # [(i + 1) * d, -2, 0], + # [(i + 1) * d, -1, 0], + # [i * d, -1, 0], + # ] + # ) + # plotter.add(c, facecolor=color, edgecolor=white, linewidth=0.5) + # plotter.add(p, facecolor=color, edgecolor=color) + # plotter.zoom_extents() + # plotter.show() diff --git a/src/compas/geometry/__init__.py b/src/compas/geometry/__init__.py index 0b4951551bd..fdf47ffa49b 100644 --- a/src/compas/geometry/__init__.py +++ b/src/compas/geometry/__init__.py @@ -239,6 +239,15 @@ is_intersection_plane_plane, ) +from ._core.nurbsutils import ( + construct_knotvector, + find_span, + compute_basisfuncs, + compute_basisfuncsderivs, + knots_and_mults_to_knotvector, + knotvector_to_knots_and_mults, +) + # ============================================================================= # Algorithms # ============================================================================= @@ -661,6 +670,12 @@ "trimesh_remesh_constrained", "trimesh_remesh_along_isoline", "trimesh_slice", + "find_span", + "compute_basisfuncs", + "compute_basisfuncsderivs", + "construct_knotvector", + "knots_and_mults_to_knotvector", + "knotvector_to_knots_and_mults", "Geometry", "Bezier", "Arc", diff --git a/src/compas/geometry/_core/nurbsutils.py b/src/compas/geometry/_core/nurbsutils.py new file mode 100644 index 00000000000..923f2996307 --- /dev/null +++ b/src/compas/geometry/_core/nurbsutils.py @@ -0,0 +1,319 @@ +from itertools import groupby + + +def construct_knotvector(degree, pointcount): + """Construct a nonperiodic (clamped), uniform knot vector for a curve with given degree and number of control points. + + This function will generate a knotvector of the form + ``[0] * (order) + [i / d for i in range(1, d)] + [1] * (order)``, with ``order = degree + 1`` and ``d = pointcount - degree``. + Therefore the length of the knotvector will be ``pointcount + degree + 1``. + + For example, if degree is 3 and the number of control points is 7, the knot vector will be ``[0, 0, 0, 0, 1/4, 2/4, 3/4, 1, 1, 1, 1]``. + + Parameters + ---------- + degree : int + Degree of the curve. + pointcount : int + The number of control points of the curve. + + Returns + ------- + list[float] + Knot vector. + + Raises + ------ + ValueError + If the number of control points is less than degree + 1. + + References + ---------- + The NURBS Book. Chapter 2. Page 66. + + """ + order = degree + 1 + + if order > pointcount: + raise ValueError("The order of the curve (degree + 1) cannot be larger than the number of control points.") + + d = pointcount - degree + return [0] * (order) + [i / d for i in range(1, d)] + [1] * (order) + + +def knotvector_to_knots_and_mults(knotvector): + """Convert a knot vector to a list of knots and multiplicities. + + Parameters + ---------- + knotvector : list[int | float] + Knot vector. + + Returns + ------- + tuple[list[int | float], list[int]] + Knots and multiplicities. + + Notes + ----- + The "standard" representation of a knot vector is a list of the form + ``[0] * (degree + 1) + [i / d for i in range(1, d)] + [1] * (degree + 1)``, with ``d = pointcount - degree``. + This representation is used, for example, in the NURBS Book and OpenCASCADe. + + Rhino uses a knot vector of the form ``[0] * (degree) + [i / d for i in range(1, d)] + [1] * (degree)``. + + """ + knots = [] + mults = [] + + for knot, multiplicity in groupby(knotvector): + knots.append(knot) + mults.append(len(list(multiplicity))) + + return knots, mults + + +def knots_and_mults_to_knotvector(knots, mults): + """Convert a list of knots and multiplicities to a knot vector. + + Parameters + ---------- + knots : list[int | float] + Knots. + mults : list[int] + Multiplicities. + + Returns + ------- + list[int | float] + Knot vector. + + Notes + ----- + The "standard" representation of a knot vector is a list of the form + ``[0] * (degree + 1) + [i / d for i in range(1, d)] + [1] * (degree + 1)``, with ``d = pointcount - degree``. + This representation is used, for example, in the NURBS Book and OpenCASCADe. + + Rhino uses a knot vector of the form ``[0] * (degree) + [i / d for i in range(1, d)] + [1] * (degree)``. + + """ + knotvector = [] + + for knot, multiplicity in zip(knots, mults): + knotvector.extend([knot] * multiplicity) + + return knotvector + + +def find_span(n, degree, knotvector, u): + """Find the knot span index for a given knot value. + + Parameters + ---------- + n : int + Number of control points minus 1. + degree : int + Degree of the curve. + knotvector : list[int | float] + Knot vector of the curve. + u : float + Parameter value. + + Returns + ------- + int + Knot span index. + + Raises + ------ + ValueError + If the parameter value is greater than the maximum knot or less than the minimum knot. + + References + ---------- + The NURBS Book. Chapter 2. Page 68. Algorithm A2.1. + + """ + if u > knotvector[-1]: + raise ValueError("Parameter value is greater than the maximum knot.") + + if u < knotvector[0]: + raise ValueError("Parameter value is less than the minimum knot.") + + if u == knotvector[n + 1]: + return n + + low = degree + high = n + 1 + mid = (low + high) // 2 + + while u < knotvector[mid] or u >= knotvector[mid + 1]: + if u < knotvector[mid]: + high = mid + else: + low = mid + mid = (low + high) // 2 + + return mid + + +def compute_basisfuncs(degree, knotvector, i, u): + """Compute the nonzero basis functions for a given parameter value. + + Parameters + ---------- + degree : int + Degree of the curve. + knotvector : list + Knot vector of the curve. + i : int + Knot span index. + u : float + Parameter value. + + Returns + ------- + list[float] + Basis functions. + + Notes + ----- + In any given knot span, :math:`\\[u_{j}, u_{j+1}\\)` at most degree + 1 of the :math:`N_{i,degree}` basis functions are nonzero, + namely the functions :math:`N_{j-degree,degree}, \\dots, N_{j,degree}`. + + References + ---------- + The NURBS Book. Chapter 2. Page 56. + The NURBS Book. Chapter 2. Page 70. Algorithm A2.2. + + """ + N = [0.0 for _ in range(degree + 1)] + left = [0.0 for _ in range(degree + 1)] + right = [0.0 for _ in range(degree + 1)] + + N[0] = 1.0 + + for j in range(1, degree + 1): + left[j] = u - knotvector[i + 1 - j] + right[j] = knotvector[i + j] - u + + saved = 0.0 + + for r in range(j): + temp = N[r] / (right[r + 1] + left[j - r]) + N[r] = saved + right[r + 1] * temp + saved = left[j - r] * temp + + N[j] = saved + + return N + + +def compute_basisfuncsderivs(degree, knotvector, i, u, n): + """Compute the derivatives of the basis functions for a given parameter value. + + Parameters + ---------- + degree : int + Degree of the curve. + knotvector : list[int | float] + Knot vector of the curve. + i : int + Knot span index. + u : float + Parameter value. + n : int + Number of derivatives to compute. + + Returns + ------- + list[float] + Derivatives of the basis functions. + + References + ---------- + The NURBS Book. Chapter 2. Page 72. Algorithm A2.3. + + """ + # output + derivs = [[0.0 for _ in range(degree + 1)] for _ in range(n + 1)] + + # Algorithm A2.2 modified to store the basis functions and knot differences + ndu = [[0.0 for _ in range(degree + 1)] for _ in range(degree + 1)] + ndu[0][0] = 1.0 + + left = [0.0 for _ in range(degree + 1)] + right = [0.0 for _ in range(degree + 1)] + + for j in range(1, degree + 1): + left[j] = u - knotvector[i + 1 - j] + right[j] = knotvector[i + j] - u + + saved = 0.0 + + for r in range(j): + # Lower triangle + ndu[j][r] = right[r + 1] + left[j - r] + + temp = ndu[r][j - 1] / ndu[j][r] + + # Upper triangle + ndu[r][j] = saved + right[r + 1] * temp + + saved = left[j - r] * temp + + ndu[j][j] = saved + + # load the basis functions + for j in range(degree + 1): + derivs[0][j] = ndu[j][degree] + + # compute the derivatives + a = [[0.0 for _ in range(degree + 1)] for _ in range(2)] + + for r in range(degree + 1): + s1 = 0 + s2 = 1 + a[0][0] = 1.0 + + for k in range(1, n + 1): + d = 0.0 + rk = r - k + pk = degree - k + + if r >= k: + a[s2][0] = a[s1][0] / ndu[pk + 1][rk] + d = a[s2][0] * ndu[rk][pk] + + if rk >= -1: + j1 = 1 + else: + j1 = -rk + + if r - 1 <= pk: + j2 = k - 1 + else: + j2 = degree - r + + for j in range(j1, j2 + 1): + a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][rk + j] + d += a[s2][j] * ndu[rk + j][pk] + + if r <= pk: + a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r] + d += a[s2][k] * ndu[r][pk] + + derivs[k][r] = d + + j = s1 + s1 = s2 + s2 = j + + # Multiply through by the correct factors + r = degree + for k in range(1, n + 1): + for j in range(degree + 1): + derivs[k][j] *= r + r *= degree - k + + return derivs diff --git a/src/compas/geometry/curves/curve.py b/src/compas/geometry/curves/curve.py index 202bf959a05..8b769ba1427 100644 --- a/src/compas/geometry/curves/curve.py +++ b/src/compas/geometry/curves/curve.py @@ -104,7 +104,7 @@ def frame(self, frame): @property def transformation(self): if not self._transformation: - self._transformation = Transformation.from_frame(self.frame) + self._transformation = Transformation.from_frame_to_frame(Frame.worldXY(), self.frame) return self._transformation @property diff --git a/src/compas/geometry/curves/line.py b/src/compas/geometry/curves/line.py index fc1f8f19e8f..062911fb56c 100644 --- a/src/compas/geometry/curves/line.py +++ b/src/compas/geometry/curves/line.py @@ -86,7 +86,6 @@ def __init__(self, start, end, **kwargs): self._point = None self._vector = None self._direction = None - self._end = None self.start = start self.end = end @@ -164,7 +163,6 @@ def vector(self): def vector(self, vector): self._vector = Vector(*vector) self._direction = None - self._end = None @property def length(self): @@ -186,15 +184,12 @@ def start(self, point): @property def end(self): - if not self._end: - self._end = self.start + self.vector - return self._end + return self.start + self.vector @end.setter def end(self, point): self._vector = Vector.from_start_end(self.start, point) self._direction = None - self._end = None @property def midpoint(self): diff --git a/src/compas/geometry/curves/nurbs.py b/src/compas/geometry/curves/nurbs.py index 1829a8fa1ee..65bd25c6d05 100644 --- a/src/compas/geometry/curves/nurbs.py +++ b/src/compas/geometry/curves/nurbs.py @@ -159,11 +159,11 @@ def knots(self): raise NotImplementedError @property - def knotsequence(self): + def multiplicities(self): raise NotImplementedError @property - def multiplicities(self): + def knotvector(self): raise NotImplementedError @property @@ -390,3 +390,18 @@ def copy(self): self.degree, self.is_periodic, ) + + def insert_knot(self): + pass + + def refine_knot(self): + pass + + def remove_knot(self): + pass + + def elevate_degree(self): + pass + + def reduce_degree(self): + pass diff --git a/src/compas/geometry/curves/nurbs_.py b/src/compas/geometry/curves/nurbs_.py new file mode 100644 index 00000000000..befd0ff30a4 --- /dev/null +++ b/src/compas/geometry/curves/nurbs_.py @@ -0,0 +1,275 @@ +from itertools import groupby + +from compas.plugins import plugin + +from compas.geometry import Point +from compas.geometry import Vector +from compas.geometry import NurbsCurve + +from compas.geometry import construct_knotvector +from compas.geometry import find_span +from compas.geometry import compute_basisfuncs +from compas.geometry import compute_basisfuncsderivs +from compas.geometry import knots_and_mults_to_knotvector + + +@plugin(category="factories", requires=None, tryfirst=True) +def new_nurbscurve(cls, *args, **kwargs): + curve = object.__new__(DefaultNurbsCurve) + curve.__init__(*args, **kwargs) + return curve + + +@plugin(category="factories", requires=None, tryfirst=True) +def new_nurbscurve_from_parameters(cls, *args, **kwargs): + return DefaultNurbsCurve.from_parameters(*args, **kwargs) + + +@plugin(category="factories", requires=None, tryfirst=True) +def new_nurbscurve_from_points(cls, *args, **kwargs): + return DefaultNurbsCurve.from_points(*args, **kwargs) + + +class DefaultNurbsCurve(NurbsCurve): + def __init__(self, name=None): + super(DefaultNurbsCurve, self).__init__(name=name) + self._points = [] + self._weights = [] + self._knotvector = [] + self._degree = 1 + self._cache = {} + + # ============================================================================== + # Data + # ============================================================================== + + # ============================================================================== + # Properties + # ============================================================================== + + @property + def points(self): + return self._points + + @points.setter + def points(self, points): + self._points = [(point if isinstance(point, Point) else Point(*point)) for point in points] + + @property + def weights(self): + if not self._weights: + self._weights = [1.0] * len(self.points) + return self._weights + + @weights.setter + def weights(self, weights): + if len(weights) != len(self.points): + raise ValueError("The number of weights should be equal to the number of points.") + self._weights = weights + + @property + def knotvector(self): + if not self._knotvector: + self._cache = {} + self._knotvector = construct_knotvector(self.degree, len(self.points)) + return self._knotvector + + @knotvector.setter + def knotvector(self, knotvector): + self._cache = {} + self._knotvector = knotvector + + @property + def knots(self): + return [knot for knot, _ in groupby(self.knotvector)] + + @property + def multiplicities(self): + return [len(list(multiplicity)) for _, multiplicity in groupby(self.knotvector)] + + @property + def degree(self): + return self._degree + + @degree.setter + def degree(self, degree): + if degree < 1: + raise ValueError("The degree of a curve should be at least 1.") + self._cache = {} + self._degree = degree + + @property + def order(self): + return self.degree + 1 + + @property + def is_rational(self): + return any(weight != 1.0 for weight in self.weights) + + @property + def is_periodic(self): + return False + + @property + def domain(self): + return self.knotvector[self.degree], self.knotvector[-(self.degree + 1)] + + # ============================================================================== + # Helpers + # ============================================================================== + + # ============================================================================== + # Constructors + # ============================================================================== + + @classmethod + def from_parameters(cls, points, weights, knots, multiplicities, degree, is_periodic=False): + """Construct a NURBS curve from explicit curve parameters. + + Parameters + ---------- + points : list[:class:`~compas.geometry.Point`] + The control points. + weights : list[float] + The control point weights. + knots : list[float] + The curve knots, without multiplicity. + multiplicities : list[int] + Multiplicity of the knots. + degree : int + The degree of the curve. + is_periodic : bool, optional + Flag indicating whether the curve is periodic or not. + Note that this parameters is currently not supported. + + Returns + ------- + :class:`compas.geometry.DefaultNurbsCurve` + + """ + curve = cls() + curve.points = points + curve.weights = weights + curve.knotvector = knots_and_mults_to_knotvector(knots, multiplicities) + curve.degree = degree + return curve + + @classmethod + def from_points(cls, points, degree=3): + """Construct a NURBS curve from a list of points. + + Parameters + ---------- + points : list[:class:`~compas.geometry.Point`] + The control points. + degree : int, optional + The degree of the curve. + + Returns + ------- + :class:`compas.geometry.DefaultNurbsCurve` + + """ + curve = cls() + curve.points = points + curve.degree = degree + return curve + + # ============================================================================== + # Methods + # ============================================================================== + + def compute_basis(self, t): + """Compute the basis values for a given parameter value. + + Parameters + ---------- + t : float + Parameter value. + + Returns + ------- + list[float] + Basis values. + + """ + if t not in self._cache: + p = self.degree + n = len(self.points) - 1 + i = find_span(n, p, self.knotvector, t) + N = compute_basisfuncs(p, self.knotvector, i, t) + self._cache[t] = i, N + return self._cache[t] + + def point_at(self, t): + """Compute a point on the curve for a given parameter value. + + Parameters + ---------- + t : float + Parameter value. + + Returns + ------- + :class:`compas.geometry.Point` + Point on the curve. + + """ + p = self.degree + i, N = self.compute_basis(t) + + points = self.points + weights = self.weights + + X = 0.0 + Y = 0.0 + Z = 0.0 + W = 0.0 + + for j in range(p + 1): + x, y, z = points[i - p + j] + b = N[j] * weights[i - p + j] + X += x * b + Y += y * b + Z += z * b + W += b + + return Point(X / W, Y / W, Z / W) + + def tangent_at(self, t, unitized=True): + """Compute the tangent of the curve for a given parameter value. + + Parameters + ---------- + t : float + Parameter value. + + Returns + ------- + :class:`compas.geometry.Vector` + Tangent vector. + + """ + d = 1 + n = len(self.points) - 1 + p = self.degree + du = min(d, p) + i = find_span(n, p, self.knotvector, t) + derivs = compute_basisfuncsderivs( + p, self.knotvector, i, t, d + ) # this should be cached and combined with basisfuncs + CK = [[0.0, 0.0, 0.0] for _ in range(d + 1)] + + for k in range(du + 1): # type: ignore + for j in range(p + 1): + x, y, z = self.points[i - p + j] + deriv = derivs[k][j] + CK[k][0] += deriv * x + CK[k][1] += deriv * y + CK[k][2] += deriv * z + + tangent = Vector(*CK[1]) + if unitized: + tangent.unitize() + + return tangent diff --git a/src/compas/geometry/curves/polyline.py b/src/compas/geometry/curves/polyline.py index e8a158c61fd..dee9f9a19d2 100644 --- a/src/compas/geometry/curves/polyline.py +++ b/src/compas/geometry/curves/polyline.py @@ -202,6 +202,32 @@ def transform(self, T): # Methods # ========================================================================== + def append(self, point): + """Append a point to the end of the polyline. + + Parameters + ---------- + point : [float, float, float] | :class:`~compas.geometry.Point` + The point to append. + + """ + self.points.append(Point(*point)) + self._lines = None + + def insert(self, i, point): + """Insert a point at the specified index. + + Parameters + ---------- + i : int + The index of the insertion point. + point : [float, float, float] | :class:`~compas.geometry.Point` + The point to insert. + + """ + self.points.insert(i, Point(*point)) + self._lines = None + def point_at(self, t, snap=False): """Point on the polyline at a specific normalized parameter. diff --git a/src/compas/geometry/shapes/cylinder.py b/src/compas/geometry/shapes/cylinder.py index 3c2e36f7fa8..1f34887ea7e 100644 --- a/src/compas/geometry/shapes/cylinder.py +++ b/src/compas/geometry/shapes/cylinder.py @@ -159,7 +159,7 @@ def plane(self): @property def circle(self): - return Circle(self.frame, self.radius) + return Circle(radius=self.radius, frame=self.frame) @property def diameter(self): diff --git a/src/compas/geometry/shapes/shape.py b/src/compas/geometry/shapes/shape.py index 140189291b5..c07760f25e4 100644 --- a/src/compas/geometry/shapes/shape.py +++ b/src/compas/geometry/shapes/shape.py @@ -79,7 +79,7 @@ def frame(self, frame): @property def transformation(self): if not self._transformation: - self._transformation = Transformation.from_frame(self.frame) + self._transformation = Transformation.from_frame_to_frame(Frame.worldXY(), self.frame) return self._transformation @property diff --git a/src/compas/geometry/shapes/sphere.py b/src/compas/geometry/shapes/sphere.py index e273c7105d9..42b1827b3ea 100644 --- a/src/compas/geometry/shapes/sphere.py +++ b/src/compas/geometry/shapes/sphere.py @@ -202,7 +202,7 @@ def to_vertices_and_faces(self, u=16, v=16, triangulated=False): phi = pi * 2 / u hpi = pi * 0.5 - x, y, z = self.frame.point + x, y, z = 0, 0, 0 vertices = [] for i in range(1, v): diff --git a/src/compas/geometry/surfaces/nurbs.py b/src/compas/geometry/surfaces/nurbs.py index 1c52d262816..59a972689fe 100644 --- a/src/compas/geometry/surfaces/nurbs.py +++ b/src/compas/geometry/surfaces/nurbs.py @@ -50,17 +50,17 @@ class NurbsSurface(Surface): The control points as rows along the U direction. weights : list[list[float]], read-only The weights of the control points. - u_knots : list[float], read-only + knots_u : list[float], read-only The knots in the U direction, without multiplicity. - v_knots : list[float], read-only + knots_v : list[float], read-only The knots in the V direction, without multiplicity. - u_mults : list[int], read-only + mults_u : list[int], read-only Multiplicity of the knots in the U direction. - v_mults : list[int], read-only + mults_v : list[int], read-only Multiplicity of the knots in the V direction. - u_degree : list[int], read-only + degree_u : list[int], read-only The degree of the surface in the U direction. - v_degree : list[int], read-only + degree_v : list[int], read-only The degree of the surface in the V direction. """ @@ -70,14 +70,14 @@ class NurbsSurface(Surface): "properties": { "points": {"type": "array", "items": {"type": "array", "items": Point.DATASCHEMA}}, "weights": {"type": "array", "items": {"type": "array", "items": {"type": "number"}}}, - "u_knots": {"type": "array", "items": {"type": "number"}}, - "v_knots": {"type": "array", "items": {"type": "number"}}, - "u_mults": {"type": "array", "items": {"type": "integer"}}, - "v_mults": {"type": "array", "items": {"type": "integer"}}, - "u_degree": {"type": "integer", "exclusiveMinimum": 0}, - "v_degree": {"type": "integer", "exclusiveMinimum": 0}, - "is_u_periodic": {"type": "boolean"}, - "is_v_periodic": {"type": "boolean"}, + "knots_u": {"type": "array", "items": {"type": "number"}}, + "knots_v": {"type": "array", "items": {"type": "number"}}, + "mults_u": {"type": "array", "items": {"type": "integer"}}, + "mults_v": {"type": "array", "items": {"type": "integer"}}, + "degree_u": {"type": "integer", "exclusiveMinimum": 0}, + "degree_v": {"type": "integer", "exclusiveMinimum": 0}, + "is_periodic_u": {"type": "boolean"}, + "is_periodic_v": {"type": "boolean"}, }, "additionalProperties": False, "minProperties": 10, @@ -90,18 +90,18 @@ def __init__(self, name=None): super(NurbsSurface, self).__init__(name=name) def __repr__(self): - return "{0}(points={1!r}, weigths={2}, u_knots={3}, v_knots={4}, u_mults={5}, v_mults={6}, u_degree={7}, v_degree={8}, is_u_periodic={9}, is_v_periodic={10})".format( + return "{0}(points={1!r}, weigths={2}, knots_u={3}, knots_v={4}, mults_u={5}, mults_v={6}, degree_u={7}, degree_v={8}, is_periodic_u={9}, is_periodic_v={10})".format( type(self).__name__, self.points, self.weights, - self.u_knots, - self.v_knots, - self.u_mults, - self.v_mults, - self.u_degree, - self.v_degree, - self.is_u_periodic, - self.is_v_periodic, + self.knots_u, + self.knots_v, + self.mults_u, + self.mults_v, + self.degree_u, + self.degree_v, + self.is_periodic_u, + self.is_periodic_v, ) # ============================================================================== @@ -117,14 +117,14 @@ def data(self): return { "points": [point.data for point in self.points], "weights": self.weights, - "u_knots": self.u_knots, - "v_knots": self.v_knots, - "u_mults": self.u_mults, - "v_mults": self.v_mults, - "u_degree": self.u_degree, - "v_degree": self.v_degree, - "is_u_periodic": self.is_u_periodic, - "is_v_periodic": self.is_v_periodic, + "knots_u": self.knots_u, + "knots_v": self.knots_v, + "mults_u": self.mults_u, + "mults_v": self.mults_v, + "degree_u": self.degree_u, + "degree_v": self.degree_v, + "is_periodic_u": self.is_periodic_u, + "is_periodic_v": self.is_periodic_v, } @classmethod @@ -145,14 +145,14 @@ def from_data(cls, data): return cls.from_parameters( data["points"], data["weights"], - data["u_knots"], - data["v_knots"], - data["u_mults"], - data["v_mults"], - data["u_degree"], - data["v_degree"], - data["is_u_periodic"], - data["is_v_periodic"], + data["knots_u"], + data["knots_v"], + data["mults_u"], + data["mults_v"], + data["degree_u"], + data["degree_v"], + data["is_periodic_u"], + data["is_periodic_v"], ) # ============================================================================== @@ -168,27 +168,35 @@ def weights(self): raise NotImplementedError @property - def u_knots(self): + def knots_u(self): raise NotImplementedError @property - def v_knots(self): + def mults_u(self): raise NotImplementedError @property - def u_mults(self): + def knotvector_u(self): raise NotImplementedError @property - def v_mults(self): + def knots_v(self): raise NotImplementedError @property - def u_degree(self): + def mults_v(self): raise NotImplementedError @property - def v_degree(self): + def knotvector_v(self): + raise NotImplementedError + + @property + def degree_u(self): + raise NotImplementedError + + @property + def degree_v(self): raise NotImplementedError # ============================================================================== @@ -200,14 +208,14 @@ def from_parameters( cls, points, weights, - u_knots, - v_knots, - u_mults, - v_mults, - u_degree, - v_degree, - is_u_periodic=False, - is_v_periodic=False, + knots_u, + knots_v, + mults_u, + mults_v, + degree_u, + degree_v, + is_periodic_u=False, + is_periodic_v=False, ): """Construct a NURBS surface from explicit parameters. @@ -217,17 +225,17 @@ def from_parameters( The control points. weights : list[list[float]] The weights of the control points. - u_knots : list[float] + knots_u : list[float] The knots in the U direction, without multiplicity. - v_knots : list[float] + knots_v : list[float] The knots in the V direction, without multiplicity. - u_mults : list[int] + mults_u : list[int] Multiplicity of the knots in the U direction. - v_mults : list[int] + mults_v : list[int] Multiplicity of the knots in the V direction. - u_degree : int + degree_u : int Degree in the U direction. - v_degree : int + degree_v : int Degree in the V direction. Returns @@ -239,27 +247,27 @@ def from_parameters( cls, points, weights, - u_knots, - v_knots, - u_mults, - v_mults, - u_degree, - v_degree, - is_u_periodic=is_u_periodic, - is_v_periodic=is_v_periodic, + knots_u, + knots_v, + mults_u, + mults_v, + degree_u, + degree_v, + is_periodic_u=is_periodic_u, + is_periodic_v=is_periodic_v, ) @classmethod - def from_points(cls, points, u_degree=3, v_degree=3): + def from_points(cls, points, degree_u=3, degree_v=3): """Construct a NURBS surface from control points. Parameters ---------- points : list[list[[float, float, float] | :class:`~compas.geometry.Point`]] The control points. - u_degree : int + degree_u : int Degree in the U direction. - v_degree : int + degree_v : int Degree in the V direction. Returns @@ -267,7 +275,7 @@ def from_points(cls, points, u_degree=3, v_degree=3): :class:`~compas.geometry.NurbsSurface` """ - return new_nurbssurface_from_points(cls, points, u_degree=u_degree, v_degree=v_degree) + return new_nurbssurface_from_points(cls, points, degree_u=degree_u, degree_v=degree_v) @classmethod def from_meshgrid(cls, nu=10, nv=10): @@ -356,12 +364,12 @@ def copy(self): return NurbsSurface.from_parameters( self.points, self.weights, - self.u_knots, - self.v_knots, - self.u_mults, - self.v_mults, - self.u_degree, - self.v_degree, - self.is_u_periodic, - self.is_v_periodic, + self.knots_u, + self.knots_v, + self.mults_u, + self.mults_v, + self.degree_u, + self.degree_v, + self.is_periodic_u, + self.is_periodic_v, ) diff --git a/src/compas/geometry/surfaces/nurbs_.py b/src/compas/geometry/surfaces/nurbs_.py new file mode 100644 index 00000000000..4cdfffe63cd --- /dev/null +++ b/src/compas/geometry/surfaces/nurbs_.py @@ -0,0 +1,343 @@ +from itertools import groupby + +from compas.plugins import plugin +from compas.utilities import flatten + +from compas.geometry import Point +from compas.geometry import NurbsSurface +from compas.geometry import construct_knotvector +from compas.geometry import find_span +from compas.geometry import compute_basisfuncs + +# from compas.geometry import compute_basisfuncsderivs + + +@plugin(category="factories", requires=None, tryfirst=True) +def new_nurbssurface(cls, *args, **kwargs): + curve = object.__new__(DefaultNurbsSurface) + curve.__init__(*args, **kwargs) + return curve + + +@plugin(category="factories", requires=None, tryfirst=True) +def new_nurbssurface_from_parameters(cls, *args, **kwargs): + return DefaultNurbsSurface.from_parameters(*args, **kwargs) + + +@plugin(category="factories", requires=None, tryfirst=True) +def new_nurbssurface_from_points(cls, *args, **kwargs): + return DefaultNurbsSurface.from_points(*args, **kwargs) + + +class DefaultNurbsSurface(NurbsSurface): + def __init__(self, name=None): + super(DefaultNurbsSurface, self).__init__(name=name) + self._points = [] + self._weights = [] + self._knotvector_u = [] + self._knotvector_v = [] + self._degree_u = 1 + self._degree_v = 1 + self._cache_u = {} + self._cache_v = {} + + # ============================================================================== + # Data + # ============================================================================== + + # ============================================================================== + # Properties + # ============================================================================== + + @property + def points(self): + """list of list of :class:`compas.geometry.Point`: + + The structure of this list is as follows: + + - assuming a surface with its U and V directions parallel to the X and Y axes, respectively; and + - assuming the U domain is [0.0, 1.0] and the V domain is [0.0, 1.0]; + - then the list of points at position points[0] defines the control points of an isocurve on the surface with fixed parameter value u = 0.0 and v going from 0.0 to 1.0. + + """ + return self._points + + @points.setter + def points(self, points): + temp = [] + for column in points: + temp.append([]) + for point in column: + if isinstance(point, Point): + temp[-1].append(point) + else: + temp[-1].append(Point(*point)) + self._points = temp + + @property + def weights(self): + if not self._weights: + self._weights = [[1.0 for point in column] for column in self.points] + return self._weights + + @weights.setter + def weights(self, weights): + self._weights = weights + + @property + def degree_u(self): + return self._degree_u + + @degree_u.setter + def degree_u(self, degree): + if degree < 1: + raise ValueError("The degree of a curve should be at least 1.") + self._cache_u = {} + self._degree_u = degree + + @property + def degree_v(self): + return self._degree_v + + @degree_v.setter + def degree_v(self, degree): + if degree < 1: + raise ValueError("The degree of a curve should be at least 1.") + self._cache_v = {} + self._degree_v = degree + + @property + def knotvector_u(self): + if not self._knotvector_u: + self.knotvector_u = construct_knotvector(self.degree_u, len(self.points)) + return self._knotvector_u + + @knotvector_u.setter + def knotvector_u(self, knotvector): + self._cache_u = {} + self._knotvector_u = knotvector + + @property + def knotvector_v(self): + if not self._knotvector_v: + self.knotvector_v = construct_knotvector(self.degree_v, len(self.points[0])) + return self._knotvector_v + + @knotvector_v.setter + def knotvector_v(self, knotvector): + self._cache_v = {} + self._knotvector_v = knotvector + + @property + def knots_u(self): + return [knot for knot, _ in groupby(self.knotvector_u)] + + @property + def mults_u(self): + return [len(list(multiplicity)) for _, multiplicity in groupby(self.knotvector_u)] + + @property + def knots_v(self): + return [knot for knot, _ in groupby(self.knotvector_v)] + + @property + def mults_v(self): + return [len(list(multiplicity)) for _, multiplicity in groupby(self.knotvector_v)] + + @property + def order_u(self): + return self.degree_u + 1 + + @property + def order_v(self): + return self.degree_v + 1 + + @property + def domain_u(self): + return self.knotvector_u[self.degree_u], self.knotvector_u[-(self.degree_u + 1)] + + @property + def domain_v(self): + return self.knotvector_v[self.degree_v], self.knotvector_v[-(self.degree_v + 1)] + + @property + def is_rational(self): + return any(weight != 1.0 for weight in flatten(self.weights)) + + @property + def is_periodic_u(self): + return False + + @property + def is_periodic_v(self): + return False + + # ============================================================================== + # Constructors + # ============================================================================== + + @classmethod + def from_parameters(cls, points, weights, degree_u, degree_v, knotvector_u, knotvector_v): + """Construct a NURBS surface from explicit surface parameters. + + Parameters + ---------- + points : list[list[:class:`compas.geometry.Point`]] + The control points of the surface. + weights : list[list[float]] + The weights of the control points. + degree_u : int + The degree of the surface in the u direction. + degree_v : int + The degree of the surface in the v direction. + knotvector_u : list[int | float] + The knot vector of the surface in the u direction. + knotvector_v : list[int | float] + The knot vector of the surface in the v direction. + + Returns + ------- + :class:`compas.geometry.DefaultNurbsSurface` + + """ + surface = cls() + surface.points = points + surface.weights = weights + surface.degree_u = degree_u + surface.degree_v = degree_v + surface.knotvector_u = knotvector_u + surface.knotvector_v = knotvector_v + return surface + + @classmethod + def from_points(cls, points, degree_u=3, degree_v=3, weights=None): + """Construct a NURBS surface from a grid of points. + + Parameters + ---------- + points : list[list[:class:`compas.geometry.Point`]] + The control points of the surface. + degree_u : int, optional + The degree of the surface in the u direction. + degree_v : int, optional + The degree of the surface in the v direction. + weights : list[list[float]], optional + The weights of the control points. + Default is ``None``, in which case all weights are set to 1.0. + + Returns + ------- + :class:`compas.geometry.DefaultNurbsSurface` + + """ + surface = cls() + surface.points = points + surface.degree_u = degree_u + surface.degree_v = degree_v + surface.weights = weights + return surface + + # ============================================================================== + # Methods + # ============================================================================== + + def point_at(self, u, v): + """Compute a point on the surface for a given parameter value. + + Parameters + ---------- + u : float + Parameter value in the u direction. + v : float + Parameter value in the v direction. + + Returns + ------- + :class:`compas.geometry.Point` + Point on the surface. + + """ + n = len(self.points) - 1 + m = len(self.points[0]) - 1 + + span_u = find_span(n, self.degree_u, self.knotvector_u, u) + span_v = find_span(m, self.degree_v, self.knotvector_v, v) + + bases_u = compute_basisfuncs(self.degree_u, self.knotvector_u, span_u, u) + bases_v = compute_basisfuncs(self.degree_v, self.knotvector_v, span_v, v) + + Sx = 0.0 + Sy = 0.0 + Sz = 0.0 + + for j in range(self.degree_v + 1): + X = 0.0 + Y = 0.0 + Z = 0.0 + + for i in range(self.degree_u + 1): + x, y, z = self.points[span_u - self.degree_u + i][span_v - self.degree_v + j] + + X += x * bases_u[i] + Y += y * bases_u[i] + Z += z * bases_u[i] + + Sx += X * bases_v[j] + Sy += Y * bases_v[j] + Sz += Z * bases_v[j] + + return Point(Sx, Sy, Sz) + + def normal_at(self, u, v): + """Compute the normal vector at a point on the surface for a given parameter value. + + Parameters + ---------- + u : float + Parameter value in the u direction. + v : float + Parameter value in the v direction. + + Returns + ------- + :class:`compas.geometry.Vector` + Normal vector at the point. + + """ + # n = len(self.points) - 1 + # m = len(self.points[0]) - 1 + + # span_u = find_span(n, self.degree_u, self.knotvector_u, u) + # span_v = find_span(m, self.degree_v, self.knotvector_v, v) + + # _, _, ders_u = compute_basisfuncsderivs(self.degree_u, self.knotvector_u, span_u, u, 1) + # _, _, ders_v = compute_basisfuncsderivs(self.degree_v, self.knotvector_v, span_v, v, 1) + + # du = min(self.degree_u, 1) + # dv = min(self.degree_v, 1) + + # Su = Vector(0.0, 0.0, 0.0) + # Sv = Vector(0.0, 0.0, 0.0) + + # for i in range(du + 1): + # Su += self.points[span_u - self.degree_u + i][span_v] * ders_u[i][0] + + # for j in range(dv + 1): + # Sv += self.points[span_u][span_v - self.degree_v + j] * ders_v[j][0] + + # return Su.cross(Sv).unitized() + + def isocurve_u(self, v): + """Construct an isocurve in the u direction at a fixed parameter value in the v direction. + + Parameters + ---------- + v : float + Parameter value in the v direction. + + Returns + ------- + :class:`compas.geometry.NurbsCurve` + + """ + # see p 110 to get started. diff --git a/src/compas/geometry/surfaces/surface.py b/src/compas/geometry/surfaces/surface.py index 35027d81c74..11b192cc5f9 100644 --- a/src/compas/geometry/surfaces/surface.py +++ b/src/compas/geometry/surfaces/surface.py @@ -39,13 +39,13 @@ class Surface(Geometry): Default is the world coordinate system. transformation : :class:`~compas.geometry.Transformation`, read-only The transformation from the surface's local coordinate system to the world coordinate system. - u_domain : tuple[float, float], read-only + domain_u : tuple[float, float], read-only The parameter domain of the surface in the U direction. - v_domain : tuple[float, float], read-only + domain_v : tuple[float, float], read-only The parameter domain of the surface in the V direction. - is_u_periodic : bool, read-only + is_periodic_u : bool, read-only Flag indicating if the surface is periodic in the U direction. - is_v_periodic : bool, read-only + is_periodic_v : bool, read-only Flag indicating if the surface is periodic in the V direction. """ @@ -57,18 +57,18 @@ def __init__(self, frame=None, name=None): super(Surface, self).__init__(name=name) self._frame = None self._transformation = None - self._u_domain = None - self._v_domain = None + self._domain_u = None + self._domain_v = None self._point = None if frame: self.frame = frame def __repr__(self): - return "{0}(frame={1!r}, u_domain={2}, v_domain={3})".format( + return "{0}(frame={1!r}, domain_u={2}, domain_v={3})".format( type(self).__name__, self.frame, - self.u_domain, - self.v_domain, + self.domain_u, + self.domain_v, ) # ============================================================================== @@ -122,27 +122,27 @@ def dimension(self): return 3 @property - def u_domain(self): - if not self._u_domain: - self._u_domain = (0.0, 1.0) - return self._u_domain + def domain_u(self): + if not self._domain_u: + self._domain_u = (0.0, 1.0) + return self._domain_u @property - def v_domain(self): - if not self._v_domain: - self._v_domain = (0.0, 1.0) - return self._v_domain + def domain_v(self): + if not self._domain_v: + self._domain_v = (0.0, 1.0) + return self._domain_v @property def is_closed(self): raise NotImplementedError @property - def is_u_periodic(self): + def is_periodic_u(self): raise NotImplementedError @property - def is_v_periodic(self): + def is_periodic_v(self): raise NotImplementedError # ============================================================================== @@ -242,12 +242,12 @@ def to_vertices_and_faces(self, nu=16, nv=16, du=None, dv=None): The faces of the surface discretisation as lists of vertex indices. """ - u_domain = du or self.u_domain - v_domain = dv or self.v_domain + domain_u = du or self.domain_u + domain_v = dv or self.domain_v vertices = [ self.point_at(i, j) - for i, j in product(linspace(u_domain[0], u_domain[1], nu + 1), linspace(v_domain[0], v_domain[1], nv + 1)) + for i, j in product(linspace(domain_u[0], domain_u[1], nu + 1), linspace(domain_v[0], domain_v[1], nv + 1)) ] faces = [ [ @@ -444,7 +444,7 @@ def u_space(self, n=10): list[float] """ - umin, umax = self.u_domain + umin, umax = self.domain_u return linspace(umin, umax, n) def v_space(self, n=10): @@ -460,7 +460,7 @@ def v_space(self, n=10): list[float] """ - vmin, vmax = self.v_domain + vmin, vmax = self.domain_v return linspace(vmin, vmax, n) def u_isocurve(self, u): diff --git a/src/compas_blender/__init__.py b/src/compas_blender/__init__.py index 06374fb7f0d..84616e4adb2 100644 --- a/src/compas_blender/__init__.py +++ b/src/compas_blender/__init__.py @@ -1,8 +1,9 @@ +# type: ignore import os import compas try: - import bpy # noqa: F401 + import bpy except ImportError: pass else: @@ -78,4 +79,5 @@ def _get_default_blender_installation_path_windows(version): __all_plugins__ = [ "compas_blender.geometry.booleans", "compas_blender.artists", + # "compas_blender.geometry.curves", ] diff --git a/src/compas_blender/artists/__init__.py b/src/compas_blender/artists/__init__.py index 4642abd1ca1..6199ef4e5a9 100644 --- a/src/compas_blender/artists/__init__.py +++ b/src/compas_blender/artists/__init__.py @@ -12,6 +12,7 @@ from compas.geometry import Frame from compas.geometry import Line from compas.geometry import Point +from compas.geometry import Pointcloud from compas.geometry import Polygon from compas.geometry import Polyhedron from compas.geometry import Polyline @@ -36,6 +37,7 @@ from .meshartist import MeshArtist from .networkartist import NetworkArtist from .pointartist import PointArtist +from .pointcloudartist import PointcloudArtist from .polygonartist import PolygonArtist from .polyhedronartist import PolyhedronArtist from .polylineartist import PolylineArtist @@ -70,6 +72,7 @@ def register_artists(): Artist.register(Mesh, MeshArtist, context="Blender") Artist.register(Network, NetworkArtist, context="Blender") Artist.register(Point, PointArtist, context="Blender") + Artist.register(Pointcloud, PointcloudArtist, context="Blender") Artist.register(Polygon, PolygonArtist, context="Blender") Artist.register(Polyhedron, PolyhedronArtist, context="Blender") Artist.register(Polyline, PolylineArtist, context="Blender") @@ -95,6 +98,7 @@ def register_artists(): "MeshArtist", "NetworkArtist", "PointArtist", + "PointcloudArtist", "PolygonArtist", "PolyhedronArtist", "PolylineArtist", diff --git a/src/compas_blender/artists/artist.py b/src/compas_blender/artists/artist.py index b535d000120..917abe2bfda 100644 --- a/src/compas_blender/artists/artist.py +++ b/src/compas_blender/artists/artist.py @@ -1,11 +1,15 @@ +from typing import Any from typing import Union from typing import Optional -from typing import Any -import bpy +import bpy # type: ignore import compas_blender +from compas.colors import Color from compas.artists import Artist +from compas.geometry import Transformation + +from compas_blender import conversions class BlenderArtist(Artist): @@ -17,30 +21,236 @@ class BlenderArtist(Artist): The Blender scene collection the object(s) created by the artist belong to. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas.artists.Artist` for more info. Attributes ---------- - collection : :blender:`bpy.types.Collection` - The collection containing the object(s) created by this artist. + objects : list[:blender:`bpy.types.Object`] + The Blender objects created by the artist. """ - def __init__(self, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): - # Initialize collection before even calling super because other classes depend on that - self._collection = None - self.collection = collection + def __init__(self, **kwargs: Any): super().__init__(**kwargs) + self.objects = [] + + # many of the methods below will be added to a general scene object in the future + # to make them universaly accessible they are added here for now + + # ============================================================================= + # Objects + # ============================================================================= + + def create_object( + self, geometry: Union[bpy.types.Mesh, bpy.types.Curve], name: Optional[str] = None + ) -> bpy.types.Object: + """Add an object to the Blender scene. + + Parameters + ---------- + geometry : :blender:`bpy.types.Mesh` | :blender:`bpy.types.Curve` + The Blender object data. + name : str, optional + The name of the object. + + Returns + ------- + :blender:`bpy.types.Object` + The Blender object. + + """ + obj = bpy.data.objects.new(name, geometry) + self.objects.append(obj) + return obj + + def update_object( + self, + obj: bpy.types.Object, + name: Optional[str] = None, + color: Optional[Color] = None, + collection: Optional[str] = None, + transformation: Optional[Transformation] = None, + show_wire: Optional[bool] = False, + ) -> None: + """Update an object in the Blender scene. + + Parameters + ---------- + obj : :blender:`bpy.types.Object` + The Blender object data. + name : str, optional + The name of the object. + color : :class:`compas.colors.Color`, optional + The color specification. + collection : str, optional + The collection to which the object should be added. + transformation : :class:`compas.geometry.Transformation`, optional + The transformation to apply to the object. + show_wire : bool, optional + Show the wireframe of the object. - @property - def collection(self) -> bpy.types.Collection: - return self._collection + Returns + ------- + None - @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) + """ + if show_wire: + obj.show_wire = True + + if name: + obj.name = name + + if color: + self.set_object_color(obj, color) + + self.set_object_tranformation(obj, transformation) + self.add_object_to_collection(obj, collection) + + def add_object_to_collection( + self, obj: bpy.types.Object, name: Optional[str] = None, do_unlink: Optional[bool] = True + ) -> bpy.types.Collection: + """Add an object to a collection. + + Parameters + ---------- + obj : :blender:`bpy.types.Object` + The Blender object. + name : str, optional + The name of the collection to which the object should be added. + + Returns + ------- + :blender:`bpy.types.Collection` + + """ + if name: + collection = self.create_collection(name) else: - raise Exception("Collection must be of type `str` or `bpy.types.Collection`.") + collection = bpy.context.scene.collection + + if do_unlink: + for c in obj.users_collection: + c.objects.unlink(obj) + + collection.objects.link(obj) + return collection + + def set_object_color(self, obj: bpy.types.Object, color: Color) -> None: + """Set the color of a Blender object. + + Parameters + ---------- + obj : :class:`bpy.types.Object` + The Blender object. + color : rgb1 | rgb255 | :class:`compas.colors.Color` + The color specification. + + Returns + ------- + None + + """ + color = Color.coerce(color) # type: ignore + if not color: + return + + material = conversions.color_to_blender_material(color) + obj.color = color.rgba + if obj.data.materials: + obj.data.materials[0] = material + else: + obj.data.materials.append(material) + obj.active_material = material + + def set_object_tranformation(self, obj: bpy.types.Object, transformation: Optional[Transformation] = None) -> None: + """Set the transformation of a Blender object. + + Parameters + ---------- + obj : :class:`bpy.types.Object` + The Blender object. + transformation : :class:`compas.geometry.Transformation` + The transformation. + + Returns + ------- + None + + """ + if transformation: + if self.transformation: + transformation = self.transformation * transformation + obj.matrix_world = conversions.transformation_to_blender(transformation) + elif self.transformation: + obj.matrix_world = conversions.transformation_to_blender(self.transformation) + + # ============================================================================= + # Collections + # ============================================================================= + + def create_collection(self, name: str) -> bpy.types.Collection: + """Create a collection with the given name. + + Parameters + ---------- + name : str + The name of the collection. + parent : bpy.types.Collection, optional + A parent collection. + + Returns + ------- + :blender:`bpy.types.Collection` + + """ + parts = name.split("::") + parent = bpy.context.scene.collection + collection = None + for index, name in enumerate(parts): + if index > 0: + name = f"{parent.name}::{name}" + if name not in bpy.data.collections: + collection = bpy.data.collections.new(name) + parent.children.link(collection) + else: + collection = bpy.data.collections[name] + parent = collection + return collection + + def clear_collection(self, name: str, include_children: Optional[bool] = True) -> None: + """Clear the objects in a collection. + + Parameters + ---------- + name : str + The name of the collection to clear. + include_children : bool, optional + Clear the children collections as well. + + Returns + ------- + None + + """ + if name not in bpy.data.collections: + return + compas_blender.clear_collection(name) + if include_children: + collection = bpy.data.collections.get(name) + for child in collection.children: + self.clear_collection(child.name) + + def delete_collection(self, name: str) -> None: + """Delete a collection. + + Parameters + ---------- + name : str + The name of the collection to delete. + + Returns + ------- + None + + """ + self.clear_collection(name) + bpy.data.collections.remove(bpy.data.collections[name]) diff --git a/src/compas_blender/artists/boxartist.py b/src/compas_blender/artists/boxartist.py index 3aee0fee0c9..4d04840f50c 100644 --- a/src/compas_blender/artists/boxartist.py +++ b/src/compas_blender/artists/boxartist.py @@ -1,84 +1,64 @@ from typing import Any -from typing import List from typing import Optional from typing import Union -import bpy -import compas_blender +import bpy # type: ignore + from compas.geometry import Box -from compas.artists import ShapeArtist from compas.colors import Color + +from compas_blender import conversions + +from compas.artists import GeometryArtist from .artist import BlenderArtist -class BoxArtist(BlenderArtist, ShapeArtist): +class BoxArtist(BlenderArtist, GeometryArtist): """Artist for drawing box shapes in Blender. Parameters ---------- box : :class:`~compas.geometry.Box` A COMPAS box. - collection : str | :blender:`bpy.types.Collection`, optional - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.ShapeArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Box - from compas_blender.artists import BoxArtist - - box = Box.from_width_height_depth(1, 1, 1) - - artist = BoxArtist(box) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Box - from compas.artists import Artist - - box = Box.from_width_height_depth(1, 1, 1) - - artist = Artist(box) - artist.draw() """ - def __init__(self, box: Box, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): + def __init__(self, box: Box, **kwargs: Any): + super().__init__(geometry=box, **kwargs) - super().__init__(shape=box, collection=collection or box.name, **kwargs) - - def draw(self, color: Optional[Color] = None) -> List[bpy.types.Object]: + def draw( + self, + color: Optional[Color] = None, + collection: Optional[Union[str, bpy.types.Collection]] = None, + show_wire: bool = True, + ) -> bpy.types.Object: """Draw the box associated with the artist. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the box. - The default color is :attr:`compas.artists.ShapeArtist.color`. + collection : str | :blender:`bpy.types.Collection`, optional + The name of the Blender scene collection containing the created object(s). + show_wire : bool, optional + Display the wireframe of the box. Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` The object(s) created in Blender to represent the box. """ color = Color.coerce(color) or self.color - vertices, faces = self.shape.to_vertices_and_faces() - obj = compas_blender.draw_mesh( - vertices, - faces, - name=self.shape.name, - color=color, - collection=self.collection, - ) - return [obj] + name = self.geometry.name + + # add option for local coordinates + vertices, faces = self.geometry.to_vertices_and_faces() + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=self.geometry.name) + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + + return obj diff --git a/src/compas_blender/artists/capsuleartist.py b/src/compas_blender/artists/capsuleartist.py index 18b003246a1..aa30eb23af4 100644 --- a/src/compas_blender/artists/capsuleartist.py +++ b/src/compas_blender/artists/capsuleartist.py @@ -1,93 +1,73 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender from compas.geometry import Capsule -from compas.artists import ShapeArtist from compas.colors import Color + +from compas_blender import conversions + +from compas.artists import GeometryArtist from .artist import BlenderArtist -class CapsuleArtist(BlenderArtist, ShapeArtist): +class CapsuleArtist(BlenderArtist, GeometryArtist): """Artist for drawing capsule shapes in Blender. Parameters ---------- capsule : :class:`~compas.geometry.Capsule` A COMPAS capsule. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.ShapeArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Capsule - from compas_blender.artists import CapsuleArtist - - capsule = Capsule(([0, 0, 0], [1, 0, 0]), 0.3) - - artist = CapsuleArtist(capsule) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Capsule - from compas.artists import Artist - - capsule = Capsule(([0, 0, 0], [1, 0, 0]), 0.3) - - artist = Artist(capsule) - artist.draw() """ - def __init__(self, capsule: Capsule, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): - - super().__init__(shape=capsule, collection=collection or capsule.name, **kwargs) - - def draw(self, color: Optional[Color] = None, u: int = None, v: int = None) -> List[bpy.types.Object]: + def __init__(self, capsule: Capsule, **kwargs: Any): + super().__init__(geometry=capsule, **kwargs) + + def draw( + self, + color: Optional[Color] = None, + collection: Optional[str] = None, + u: int = 16, + v: int = 16, + show_wire: bool = False, + shade_smooth: bool = True, + ) -> bpy.types.Object: """Draw the capsule associated with the artist. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the capsule. - The default color is :attr:`compas.artists.ShapeArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). u : int, optional Number of faces in the "u" direction. - Default is :attr:`CapsuleArtist.u`. v : int, optional Number of faces in the "v" direction. - Default is :attr:`CapsuleArtist.v`. + show_wire : bool, optional + Display the wireframe of the capsule. + shade_smooth : bool, optional + Display smooth shading on the capsule. Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` The objects created in Blender. """ - u = u or self.u - v = v or self.v + name = self.geometry.name color = Color.coerce(color) or self.color - vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) - obj = compas_blender.draw_mesh( - vertices, - faces, - name=self.shape.name, - color=color, - collection=self.collection, - ) - return [obj] + + vertices, faces = self.geometry.to_vertices_and_faces(u=u, v=v) + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=self.geometry.name) + if shade_smooth: + mesh.shade_smooth() + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + + return obj diff --git a/src/compas_blender/artists/circleartist.py b/src/compas_blender/artists/circleartist.py index 913d1b9989a..ba5234f2c05 100644 --- a/src/compas_blender/artists/circleartist.py +++ b/src/compas_blender/artists/circleartist.py @@ -1,121 +1,52 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender -from compas.geometry import add_vectors from compas.geometry import Circle -from compas.artists import PrimitiveArtist from compas.colors import Color + +from compas.artists import GeometryArtist from .artist import BlenderArtist -class CircleArtist(BlenderArtist, PrimitiveArtist): +class CircleArtist(BlenderArtist, GeometryArtist): """Artist for drawing circles in Blender. Parameters ---------- circle : :class:`~compas.geometry.Circle` A COMPAS circle. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Plane, Circle - from compas_blender.artists import CircleArtist - - circle = Circle(Plane([0, 0, 0], [0,, 0, 1]), 1.0) - - artist = CircleArtist(circle) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Plane, Circle - from compas.artists import Artist - - circle = Circle(Plane([0, 0, 0], [0,, 0, 1]), 1.0) - - artist = Artist(circle) - artist.draw() """ - def __init__(self, circle: Circle, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): - super().__init__(primitive=circle, collection=collection or circle.name, **kwargs) + def __init__(self, circle: Circle, **kwargs: Any): + super().__init__(geometry=circle, **kwargs) - def draw( - self, - color: Optional[Color] = None, - show_point: bool = False, - show_normal: bool = False, - ) -> List[bpy.types.Object]: + def draw(self, color: Optional[Color] = None, collection: Optional[str] = None) -> bpy.types.Object: """Draw the circle. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the capsule. - The default color is :attr:`compas.artists.PrimitiveArtist.color`. - show_point : bool, optional - If True, also draw the center point of the circle. - show_normal : bool, optional - If True, also draw the normal vector of the circle. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- - list[:blender:`bpy.types.Object`] - The objects created in Blender. + :blender:`bpy.types.Object` + The object created in Blender. """ color = Color.coerce(color) or self.color - point = self.primitive.plane.point - normal = self.primitive.plane.normal - plane = point, normal - radius = self.primitive.radius - objects = [] - if show_point: - points = [ - { - "pos": point, - "color": color, - "name": self.primitive.name, - "radius": 0.01, - } - ] - objects += compas_blender.draw_points(points, collection=self.collection) - if show_normal: - end = add_vectors(point, normal) - lines = [ - { - "start": point, - "end": end, - "color": color, - "name": self.primitive.name, - } - ] - objects += compas_blender.draw_lines(lines, collection=self.collection) - circles = [ - { - "plane": plane, - "radius": radius, - "color": color, - "name": self.primitive.name, - } - ] - objects += compas_blender.draw_circles(circles, collection=self.collection) - return objects + + bpy.ops.curve.primitive_bezier_circle_add(radius=self.geometry.radius) + + obj = bpy.context.object + self.objects.append(obj) + self.update_object(obj, color=color, collection=collection, transformation=self.geometry.transformation) + + return obj diff --git a/src/compas_blender/artists/coneartist.py b/src/compas_blender/artists/coneartist.py index 2979018999d..24d637ef323 100644 --- a/src/compas_blender/artists/coneartist.py +++ b/src/compas_blender/artists/coneartist.py @@ -1,89 +1,70 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender from compas.geometry import Cone -from compas.artists import ShapeArtist from compas.colors import Color + +from compas_blender import conversions + +from compas.artists import GeometryArtist from .artist import BlenderArtist -class ConeArtist(BlenderArtist, ShapeArtist): +class ConeArtist(BlenderArtist, GeometryArtist): """Artist for drawing cone shapes in Blender. Parameters ---------- cone : :class:`~compas.geometry.Cone` A COMPAS cone. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.ShapeArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Plane, Circle, Cone - from compas_blender.artists import ConeArtist - - cone = Cone(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0) - - artist = ConeArtist(cone) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Plane, Circle, Cone - from compas.artists import Artist - - cone = Cone(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0) - - artist = Artist(cone) - artist.draw() """ - def __init__(self, cone: Cone, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): - - super().__init__(shape=cone, collection=collection or cone.name, **kwargs) - - def draw(self, color: Optional[Color] = None, u: int = None) -> List[bpy.types.Object]: + def __init__(self, cone: Cone, **kwargs: Any): + super().__init__(geometry=cone, **kwargs) + + def draw( + self, + color: Optional[Color] = None, + collection: Optional[str] = None, + u: int = 16, + show_wire: bool = False, + shade_smooth: bool = True, + ) -> bpy.types.Object: """Draw the cone associated with the artist. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the cone. - The default color is :attr:`compas.artists.ShapeArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). u : int, optional Number of faces in the "u" direction. - Default is :attr:`ConeArtist.u`. + show_wire : bool, optional + Display the wireframe of the cone. + shade_smooth : bool, optional + Display smooth shading on the cone. Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` The objects created in Blender. """ - u = u or self.u + name = self.geometry.name color = Color.coerce(color) or self.color - vertices, faces = self.shape.to_vertices_and_faces(u=u) - obj = compas_blender.draw_mesh( - vertices, - faces, - name=self.shape.name, - color=color, - collection=self.collection, - ) - return [obj] + + vertices, faces = self.geometry.to_vertices_and_faces(u=u) + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=self.geometry.name) + if shade_smooth: + mesh.shade_smooth() + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + + return obj diff --git a/src/compas_blender/artists/curveartist.py b/src/compas_blender/artists/curveartist.py index a9404cca910..17c446c9bda 100644 --- a/src/compas_blender/artists/curveartist.py +++ b/src/compas_blender/artists/curveartist.py @@ -1,77 +1,55 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender -from compas.artists import CurveArtist from compas.geometry import Curve from compas.colors import Color -from compas_blender.artists import BlenderArtist +from compas_blender import conversions +from compas.artists import GeometryArtist +from .artist import BlenderArtist -class CurveArtist(BlenderArtist, CurveArtist): + +class CurveArtist(BlenderArtist, GeometryArtist): """Artist for drawing curves in Blender. Parameters ---------- curve : :class:`~compas.geometry.Curve` A COMPAS curve. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import NurbsCurve - from compas_blender.artists import CurveArtist - - curve = NurbsCurve([[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]]) - - artist = CurveArtist(curve) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import NurbsCurve - from compas.artists import Artist - - curve = NurbsCurve([[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]]) - - artist = Artist(curve) - artist.draw() """ - def __init__(self, curve: Curve, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): + def __init__(self, curve: Curve, **kwargs: Any): + super().__init__(geometry=curve, **kwargs) - super().__init__(curve=curve, collection=collection or curve.name, **kwargs) - - def draw(self, color: Optional[Color] = None) -> List[bpy.types.Object]: + def draw( + self, + color: Optional[Color] = None, + collection: Optional[str] = None, + ) -> bpy.types.Object: """Draw the curve. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the curve. - The default color is :attr:`compas.artists.CurveArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` """ + name = self.geometry.name color = Color.coerce(color) or self.color - curves = [{"curve": self.curve, "color": color, "name": self.curve.name}] - return compas_blender.draw_curves(curves, collection=self.collection) + curve = conversions.nurbscurve_to_blender_curve(self.geometry) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) + + return obj diff --git a/src/compas_blender/artists/cylinderartist.py b/src/compas_blender/artists/cylinderartist.py index e4998dadf95..b50eb72cdc4 100644 --- a/src/compas_blender/artists/cylinderartist.py +++ b/src/compas_blender/artists/cylinderartist.py @@ -1,90 +1,70 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender from compas.geometry import Cylinder -from compas.artists import ShapeArtist from compas.colors import Color + +from compas_blender import conversions + +from compas.artists import GeometryArtist from .artist import BlenderArtist -class CylinderArtist(BlenderArtist, ShapeArtist): +class CylinderArtist(BlenderArtist, GeometryArtist): """Artist for drawing cylinder shapes in Blender. Parameters ---------- cylinder : :class:`~compas.geometry.Cylinder` A COMPAS cylinder. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.ShapeArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Plane, Circle, Cylinder - from compas_blender.artists import CylinderArtist - - cylinder = Cylinder(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0) - - artist = CylinderArtist(cylinder) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Plane, Circle, Cylinder - from compas.artists import Artist - - cylinder = Cylinder(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0) - - artist = Artist(cylinder) - artist.draw() """ - def __init__( - self, cylinder: Cylinder, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any - ): - - super().__init__(shape=cylinder, collection=collection or cylinder.name, **kwargs) - - def draw(self, color: Optional[Color] = None, u: int = None) -> List[bpy.types.Object]: + def __init__(self, cylinder: Cylinder, **kwargs: Any): + super().__init__(geometry=cylinder, **kwargs) + + def draw( + self, + color: Optional[Color] = None, + collection: Optional[str] = None, + u: int = 16, + show_wire: bool = False, + shade_smooth: bool = True, + ) -> bpy.types.Object: """Draw the cylinder associated with the artist. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the cylinder. + collection : str, optional + The name of the Blender scene collection containing the created object(s). u : int, optional Number of faces in the "u" direction. - Default is :attr:`~CylinderArtist.u`. + show_wire : bool, optional + Display the wireframe of the cylinder. + shade_smooth : bool, optional + Display smooth shading on the cylinder. Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` The objects created in Blender. """ - u = u or self.u + name = self.geometry.name color = Color.coerce(color) or self.color - vertices, faces = self.shape.to_vertices_and_faces(u=u) - obj = compas_blender.draw_mesh( - vertices, - faces, - name=self.shape.name, - color=color, - collection=self.collection, - ) - return [obj] + + vertices, faces = self.geometry.to_vertices_and_faces(u=u) + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=self.geometry.name) + if shade_smooth: + mesh.shade_smooth() + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + + return obj diff --git a/src/compas_blender/artists/frameartist.py b/src/compas_blender/artists/frameartist.py index a1d9087365e..5db898c2e0c 100644 --- a/src/compas_blender/artists/frameartist.py +++ b/src/compas_blender/artists/frameartist.py @@ -1,39 +1,30 @@ from typing import Any from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore from compas.geometry import Frame - -import compas_blender -from compas.artists import PrimitiveArtist from compas.colors import Color + +from compas.artists import GeometryArtist from .artist import BlenderArtist -class FrameArtist(BlenderArtist, PrimitiveArtist): +class FrameArtist(BlenderArtist, GeometryArtist): """Artist for drawing frames in Blender. Parameters ---------- frame: :class:`~compas.geometry.Frame` A COMPAS frame. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. - scale: float, optional - Scale factor that controls the length of the axes. **kwargs : dict, optional Additional keyword arguments. For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. + see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.GeometryArtist`. Attributes ---------- - scale : float - Scale factor that controls the length of the axes. - Default is ``1.0``. color_origin : :class:`~compas.colors.Color` Color for the point at the frame origin. Default is ``Color.black()``. @@ -46,84 +37,72 @@ class FrameArtist(BlenderArtist, PrimitiveArtist): """ - def __init__( - self, - frame: Frame, - collection: Optional[Union[str, bpy.types.Collection]] = None, - scale: float = 1.0, - **kwargs: Any, - ): - - super().__init__(primitive=frame, collection=collection or frame.name, **kwargs) - - self.scale = scale or 1.0 + def __init__(self, frame: Frame, **kwargs: Any): + super().__init__(geometry=frame, **kwargs) self.color_origin = Color.black() self.color_xaxis = Color.red() self.color_yaxis = Color.green() self.color_zaxis = Color.blue() - def draw(self) -> List[bpy.types.Object]: + def draw( + self, + scale=1.0, + collection: Optional[str] = None, + ) -> List[bpy.types.Object]: """Draw the frame. - Returns - ------- - list[:blender:`bpy.types.Object`] - - """ - self.clear() - objects = [] - objects += self.draw_origin() - objects += self.draw_axes() - return objects - - def draw_origin(self) -> List[bpy.types.Object]: - """Draw the origin of the frame. + Parameters + ---------- + scale : float, optional + Scale of the frame axes. + collection : str, optional + The Blender scene collection containing the created objects. Returns ------- list[:blender:`bpy.types.Object`] """ - points = [ - { - "pos": self.primitive.point, - "name": f"{self.primitive.name}.origin", - "color": self.color_origin, - "radius": 0.01, - } - ] - return compas_blender.draw_points(points, self.collection) - - def draw_axes(self) -> List[bpy.types.Object]: - """Draw the axes of the frame. + objects = [] - Returns - ------- - list[:blender:`bpy.types.Object`] + name = self.geometry.name + collection = collection or name + + bpy.ops.mesh.primitive_uv_sphere_add( + location=self.geometry, + radius=0.01, + segments=16, + ring_count=16, + ) + obj = bpy.context.object + objects.append(obj) + + self.update_object(obj, color=self.color_origin, collection=collection) + + # origin = self.geometry.point + # X = self.geometry.point + self.geometry.xaxis.scaled(self.scale) + # Y = self.geometry.point + self.geometry.yaxis.scaled(self.scale) + # Z = self.geometry.point + self.geometry.zaxis.scaled(self.scale) + # lines = [ + # { + # "start": origin, + # "end": X, + # "color": self.color_xaxis, + # "name": f"{self.geometry.name}.xaxis", + # }, + # { + # "start": origin, + # "end": Y, + # "color": self.color_yaxis, + # "name": f"{self.geometry.name}.yaxis", + # }, + # { + # "start": origin, + # "end": Z, + # "color": self.color_zaxis, + # "name": f"{self.geometry.name}.zaxis", + # }, + # ] + # return compas_blender.draw_lines(lines, self.collection) - """ - origin = self.primitive.point - X = self.primitive.point + self.primitive.xaxis.scaled(self.scale) - Y = self.primitive.point + self.primitive.yaxis.scaled(self.scale) - Z = self.primitive.point + self.primitive.zaxis.scaled(self.scale) - lines = [ - { - "start": origin, - "end": X, - "color": self.color_xaxis, - "name": f"{self.primitive.name}.xaxis", - }, - { - "start": origin, - "end": Y, - "color": self.color_yaxis, - "name": f"{self.primitive.name}.yaxis", - }, - { - "start": origin, - "end": Z, - "color": self.color_zaxis, - "name": f"{self.primitive.name}.zaxis", - }, - ] - return compas_blender.draw_lines(lines, self.collection) + return objects diff --git a/src/compas_blender/artists/lineartist.py b/src/compas_blender/artists/lineartist.py index 1b15a35a62b..e2b0556d57f 100644 --- a/src/compas_blender/artists/lineartist.py +++ b/src/compas_blender/artists/lineartist.py @@ -1,110 +1,60 @@ from typing import Any from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender -from compas.artists import PrimitiveArtist from compas.geometry import Line from compas.colors import Color + +from compas_blender import conversions + +from compas.artists import GeometryArtist from compas_blender.artists import BlenderArtist -class LineArtist(BlenderArtist, PrimitiveArtist): +class LineArtist(BlenderArtist, GeometryArtist): """Artist for drawing lines in Blender. Parameters ---------- line : :class:`~compas.geometry.Line` A COMPAS line. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Line - from compas_blender.artists import LineArtist - - line = Line([0, 0, 0], [1, 1, 1]) - - artist = LineArtist(line) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Line - from compas.artists import Artist - - line = Line([0, 0, 0], [1, 1, 1]) - - artist = Artist(line) - artist.draw() + see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.GeometryArtist`. """ - def __init__( - self, - line: Line, - collection: Optional[Union[str, bpy.types.Collection]] = None, - **kwargs: Any, - ): - super().__init__(primitive=line, collection=collection or line.name, **kwargs) + def __init__(self, line: Line, **kwargs: Any): + super().__init__(geometry=line, **kwargs) - def draw(self, color: Optional[Color] = None, show_points: bool = False) -> List[bpy.types.Object]: + def draw( + self, + color: Optional[Color] = None, + collection: Optional[str] = None, + ) -> List[bpy.types.Object]: """Draw the line. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the box. - The default color is :attr:`compas.artists.PrimitiveArtist.color`. - show_points : bool, optional - If True, show the start and end point in addition to the line. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` """ + name = self.geometry.name color = Color.coerce(color) or self.color - start = self.primitive.start - end = self.primitive.end - objects = [] - if show_points: - points = [ - { - "pos": start, - "name": f"{self.primitive.name}.start", - "color": color, - "radius": 0.01, - }, - { - "pos": end, - "name": f"{self.primitive.name}.end", - "color": color, - "radius": 0.01, - }, - ] - objects += compas_blender.draw_points(points, collection=self.collection) - lines = [ - { - "start": start, - "end": end, - "color": color, - "name": f"{self.primitive.name}", - }, - ] - objects += compas_blender.draw_lines(lines, collection=self.collection) - return objects + + curve = conversions.line_to_blender_curve(self.geometry) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=True) + + return obj diff --git a/src/compas_blender/artists/meshartist.py b/src/compas_blender/artists/meshartist.py index b2591cef744..c2b65acd99a 100644 --- a/src/compas_blender/artists/meshartist.py +++ b/src/compas_blender/artists/meshartist.py @@ -5,199 +5,117 @@ from typing import Tuple from typing import Union -import bpy +import bpy # type: ignore import compas_blender from compas.datastructures import Mesh +from compas.geometry import Line +from compas.geometry import Sphere +from compas.geometry import Cylinder from compas.geometry import add_vectors from compas.geometry import centroid_points from compas.geometry import scale_vector - from compas.colors import Color -from compas.artists import MeshArtist + +from compas.artists import MeshArtist as BaseArtist from .artist import BlenderArtist +from compas_blender import conversions + -class MeshArtist(BlenderArtist, MeshArtist): +class MeshArtist(BlenderArtist, BaseArtist): """Artist for drawing mesh data structures in Blender. Parameters ---------- mesh : :class:`~compas.datastructures.Mesh` A COMPAS mesh. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. - - Attributes - ---------- - vertexcollection : :blender:`bpy.types.Collection` - The collection containing the vertices. - edgecollection : :blender:`bpy.types.Collection` - The collection containing the edges. - facecollection : :blender:`bpy.types.Collection` - The collection containing the faces. - vertexlabelcollection : :blender:`bpy.types.Collection` - The collection containing the vertex labels. - edgelabelcollection : :blender:`bpy.types.Collection` - The collection containing the edge labels. - facelabelcollection : :blender:`bpy.types.Collection` - The collection containing the face labels. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.datastructures import Mesh - from compas_blender.artists import MeshArtist - - mesh = Mesh.from_meshgrid(10, 10) - - artist = MeshArtist(mesh) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.datastructures import Mesh - from compas.artists import Artist - - mesh = Mesh.from_meshgrid(10, 10) - - artist = Artist(mesh) - artist.draw() """ - 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) - - @property - def vertexcollection(self) -> bpy.types.Collection: - if not self._vertexcollection: - self._vertexcollection = compas_blender.create_collection("Vertices", parent=self.collection) - return self._vertexcollection - - @property - def edgecollection(self) -> bpy.types.Collection: - if not self._edgecollection: - self._edgecollection = compas_blender.create_collection("Edges", parent=self.collection) - return self._edgecollection - - @property - def facecollection(self) -> bpy.types.Collection: - if not self._facecollection: - self._facecollection = compas_blender.create_collection("Faces", parent=self.collection) - return self._facecollection - - @property - def vertexnormalcollection(self) -> bpy.types.Collection: - if not self._vertexnormalcollection: - self._vertexnormalcollection = compas_blender.create_collection("VertexNormals", parent=self.collection) - return self._vertexnormalcollection - - @property - def facenormalcollection(self) -> bpy.types.Collection: - if not self._facenormalcollection: - self._facenormalcollection = compas_blender.create_collection("FaceNormals", parent=self.collection) - return self._facenormalcollection - - @property - def vertexlabelcollection(self) -> bpy.types.Collection: - if not self._vertexlabelcollection: - self._vertexlabelcollection = compas_blender.create_collection("VertexLabels", parent=self.collection) - return self._vertexlabelcollection - - @property - def edgelabelcollection(self) -> bpy.types.Collection: - if not self._edgelabelcollection: - self._edgelabelcollection = compas_blender.create_collection("EdgeLabels", parent=self.collection) - return self._edgelabelcollection - - @property - def facelabelcollection(self) -> bpy.types.Collection: - if not self._facelabelcollection: - self._facelabelcollection = compas_blender.create_collection("FaceLabels", parent=self.collection) - return self._facelabelcollection + def __init__(self, mesh: Mesh, **kwargs: Any): + super().__init__(mesh=mesh, **kwargs) + self.vertexobjects = [] + self.edgeobjects = [] + self.faceobjects = [] # ========================================================================== # clear # ========================================================================== def clear(self): - compas_blender.delete_objects(self.collection.objects) + compas_blender.delete_objects(self.objects) def clear_vertices(self): - """Clear the objects contained in the vertex collection (``self.vertexcollection``). + """Clear the vertex objects. Returns ------- None """ - compas_blender.delete_objects(self.vertexcollection.objects) + compas_blender.delete_objects(self.vertexobjects) def clear_edges(self): - """Clear the objects contained in the edge collection (``self.edgecollection``). + """Clear the edge objects. Returns ------- None """ - compas_blender.delete_objects(self.edgecollection.objects) + compas_blender.delete_objects(self.edgeobjects) def clear_faces(self): - """Clear the objects contained in the face collection (``self.facecollection``). + """Clear the face objects. Returns ------- None """ - compas_blender.delete_objects(self.facecollection.objects) + compas_blender.delete_objects(self.faceobjects) # ========================================================================== # draw # ========================================================================== - def draw(self, color: Optional[Color] = None) -> List[bpy.types.Object]: + def draw( + self, color: Optional[Color] = None, collection: Optional[str] = None, show_wire: bool = True + ) -> bpy.types.Object: """Draw the mesh. Parameters ---------- color : :class:`~compas.colors.Color`, optional The color of the mesh. - The default value is :attr:`color`. + collection : str, optional + The name of the collection that should contain the mesh. + show_wire : bool, optional + Display the wireframe of the mesh. Returns ------- list[:blender:`bpy.types.Object`] """ - self.color = color - vertices, faces = self.mesh.to_vertices_and_faces() - obj = compas_blender.draw_mesh( - vertices, - faces, - color=self.color, - name=self.mesh.name, - collection=self.collection, - ) - return [obj] + name = self.mesh.name # type: ignore + color = Color.coerce(color) or self.color + mesh = conversions.mesh_to_blender(self.mesh) # type: ignore + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + + return obj def draw_vertices( self, vertices: Optional[List[int]] = None, color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, + radius: float = 0.01, + u: int = 16, + v: int = 16, ) -> List[bpy.types.Object]: """Draw a selection of vertices. @@ -208,31 +126,46 @@ def draw_vertices( Default is None, in which case all vertices are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color specification for the vertices. - The default color of vertices is :attr:`default_vertexcolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). + + Other Parameters + ---------------- + radius : float, optional + The radius of the vertex spheres. + u : int, optional + Number of faces in the "u" direction of the vertex spheres. + v : int, optional + Number of faces in the "v" direction of the vertex spheres. Returns ------- list[:blender:`bpy.types.Object`] """ + objects = [] + self.vertex_color = color - vertices = vertices or self.vertices - points = [] - for vertex in vertices: - points.append( - { - "pos": self.vertex_xyz[vertex], - "name": f"{self.mesh.name}.vertex.{vertex}", - "color": self.vertex_color[vertex], - "radius": 0.01, - } - ) - return compas_blender.draw_points(points, self.vertexcollection) + + for vertex in vertices or self.mesh.vertices(): # type: ignore + name = f"{self.mesh.name}.vertex.{vertex}" # type: ignore + color = self.vertex_color[vertex] # type: ignore + point = self.vertex_xyz[vertex] + + # there is no such thing as a sphere data block + bpy.ops.mesh.primitive_uv_sphere_add(location=point, radius=radius, segments=u, ring_count=v) + obj = bpy.context.object + self.objects.append(obj) + self.update_object(obj, name=name, color=color, collection=collection) # type: ignore + objects.append(obj) + + return objects def draw_edges( self, edges: Optional[List[Tuple[int, int]]] = None, - color: Optional[Union[Color, Dict[int, Color]]] = None, + color: Optional[Union[Color, Dict[Tuple[int, int], Color]]] = None, + collection: Optional[str] = None, ) -> List[bpy.types.Object]: """Draw a selection of edges. @@ -243,32 +176,35 @@ def draw_edges( The default is None, in which case all edges are drawn. color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional The color specification for the edges. - The default color of edges is :attr:`default_edgecolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ + objects = [] + self.edge_color = color - edges = edges or self.edges - lines = [] - for edge in edges: - u, v = edge - lines.append( - { - "start": self.vertex_xyz[u], - "end": self.vertex_xyz[v], - "color": self.edge_color[edge], - "name": f"{self.mesh.name}.edge.{u}-{v}", - } - ) - return compas_blender.draw_lines(lines, self.edgecollection) + + for u, v in edges or self.mesh.edges(): # type: ignore + name = f"{self.mesh.name}.edge.{u}-{v}" # type: ignore + color = self.edge_color[u, v] # type: ignore + curve = conversions.line_to_blender_curve(Line(self.vertex_xyz[u], self.vertex_xyz[v])) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) # type: ignore + objects.append(obj) + + return objects def draw_faces( self, faces: Optional[List[int]] = None, color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, + show_wire: bool = True, ) -> List[bpy.types.Object]: """Draw a selection of faces. @@ -279,25 +215,31 @@ def draw_faces( The default is None, in which case all faces are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color specification for the faces. - Th default color of faces is :attr:`default_facecolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). + show_wire : bool, optional + Display the wireframe of the faces. Returns ------- list[:blender:`bpy.types.Object`] """ + objects = [] + 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[face], - } - ) - return compas_blender.draw_faces(facets, self.facecollection) + + for face in faces or self.mesh.faces(): # type: ignore + name = f"{self.mesh.name}.face.{face}" # type: ignore + color = self.face_color[face] # type: ignore + points = [self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)] # type: ignore + mesh = conversions.polygon_to_blender_mesh(points, name=name) # type: ignore + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) # type: ignore + objects.append(obj) + + return objects # ========================================================================== # draw normals @@ -308,6 +250,7 @@ def draw_vertexnormals( vertices: Optional[List[int]] = None, color: Color = Color.green(), scale: float = 1.0, + collection: Optional[str] = None, ) -> List[bpy.types.Object]: """Draw the normals at the vertices of the mesh. @@ -320,33 +263,40 @@ def draw_vertexnormals( The color specification of the normal vectors. scale : float, optional Scale factor for the vertex normals. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ - vertices = vertices or self.vertices - lines = [] - for vertex in vertices: + objects = [] + + color = Color.coerce(color) # type: ignore + + for vertex in vertices or self.mesh.vertices(): # type: ignore + name = f"{self.mesh.name}.vertex.{vertex}.normal" # type: ignore + a = self.vertex_xyz[vertex] - n = self.mesh.vertex_normal(vertex) + n = self.mesh.vertex_normal(vertex) # type: ignore b = add_vectors(a, scale_vector(n, scale)) - lines.append( - { - "start": a, - "end": b, - "color": color, - "name": f"{self.mesh.name}.vertexnormal.{vertex}", - } - ) - return compas_blender.draw_lines(lines, collection=self.vertexnormalcollection) + + curve = conversions.line_to_blender_curve(Line(a, b)) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) + + objects.append(obj) + + return objects def draw_facenormals( self, faces: Optional[List[List[int]]] = None, color: Color = Color.cyan(), scale: float = 1.0, + collection: Optional[str] = None, ) -> List[bpy.types.Object]: """Draw the normals of the faces. @@ -359,110 +309,201 @@ def draw_facenormals( The color specification of the normal vectors. scale : float, optional Scale factor for the face normals. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ - faces = faces or self.faces - lines = [] - for face in faces: - a = centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]) - n = self.mesh.face_normal(face) + objects = [] + + color = Color.coerce(color) # type: ignore + + for face in faces or self.mesh.faces(): # type: ignore + name = f"{self.mesh.name}.face.{face}.normal" # type: ignore + + a = centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]) # type: ignore + n = self.mesh.face_normal(face) # type: ignore b = add_vectors(a, scale_vector(n, scale)) - lines.append( - { - "start": a, - "end": b, - "name": f"{self.mesh.name}.facenormal.{face}", - "color": color, - } - ) - return compas_blender.draw_lines(lines, collection=self.facenormalcollection) + + curve = conversions.line_to_blender_curve(Line(a, b)) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) + + objects.append(obj) + + return objects # ========================================================================== # draw labels # ========================================================================== - def draw_vertexlabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: - """Draw labels for a selection vertices. + # def draw_vertexlabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: + # """Draw labels for a selection vertices. + + # Parameters + # ---------- + # text : dict[int, str], optional + # A dictionary of vertex labels as vertex-text pairs. + # The default value is None, in which case every vertex will be labeled with its identifier. + + # Returns + # ------- + # list[:blender:`bpy.types.Object`] + + # """ + # self.vertex_text = text + # labels = [] + # for vertex in self.vertex_text: + # labels.append( + # { + # "pos": self.vertex_xyz[vertex], + # "name": f"{self.mesh.name}.vertexlabel.{vertex}", + # "text": self.vertex_text[vertex], + # "color": self.vertex_color[vertex], + # } + # ) + # return compas_blender.draw_texts(labels, collection=self.vertexlabelcollection) + + # def draw_edgelabels(self, text: Optional[Dict[Tuple[int, int], str]] = None) -> List[bpy.types.Object]: + # """Draw labels for a selection of edges. + + # Parameters + # ---------- + # text : dict[tuple[int, int], str], optional + # A dictionary of edge labels as edge-text pairs. + # The default value is None, in which case every edge will be labeled with its identifier. + + # Returns + # ------- + # list[:blender:`bpy.types.Object`] + + # """ + # self.edge_text = text + # labels = [] + # for edge in self.edge_text: + # u, v = edge + # labels.append( + # { + # "pos": centroid_points([self.vertex_xyz[u], self.vertex_xyz[v]]), + # "name": f"{self.mesh.name}.edgelabel.{u}-{v}", + # "text": self.edge_text[edge], + # "color": self.edge_color[edge], + # } + # ) + # return compas_blender.draw_texts(labels, collection=self.edgelabelcollection) + + # def draw_facelabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: + # """Draw labels for a selection of faces. + + # Parameters + # ---------- + # text : dict[int, str], optional + # A dictionary of face labels as face-text pairs. + # The default value is None, in which case every face will be labeled with its identifier. + + # Returns + # ------- + # list[:blender:`bpy.types.Object`] + + # """ + # self.face_text = text + # labels = [] + # for face in self.face_text: + # labels.append( + # { + # "pos": centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]), + # "name": "{}.facelabel.{}".format(self.mesh.name, face), + # "text": self.face_text[face], + # "color": self.face_color[face], + # } + # ) + # return compas_blender.draw_texts(labels, collection=self.collection) + + # ============================================================================= + # draw miscellaneous + # ============================================================================= + + def draw_spheres( + self, + radius: Dict[int, float], + color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, + ) -> list[bpy.types.Object]: + """Draw spheres at the vertices of the mesh. Parameters ---------- - text : dict[int, str], optional - A dictionary of vertex labels as vertex-text pairs. - The default value is None, in which case every vertex will be labeled with its identifier. + radius : dict[int, float], optional + The radius of the spheres. + color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color of the spheres. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ - self.vertex_text = text - labels = [] - for vertex in self.vertex_text: - labels.append( - { - "pos": self.vertex_xyz[vertex], - "name": f"{self.mesh.name}.vertexlabel.{vertex}", - "text": self.vertex_text[vertex], - "color": self.vertex_color[vertex], - } - ) - return compas_blender.draw_texts(labels, collection=self.vertexlabelcollection) - - def draw_edgelabels(self, text: Optional[Dict[Tuple[int, int], str]] = None) -> List[bpy.types.Object]: - """Draw labels for a selection of edges. + objects = [] - Parameters - ---------- - text : dict[tuple[int, int], str], optional - A dictionary of edge labels as edge-text pairs. - The default value is None, in which case every edge will be labeled with its identifier. + self.vertex_color = color - Returns - ------- - list[:blender:`bpy.types.Object`] + for vertex in radius: + name = "{}.vertex.{}.sphere".format(self.mesh.name, vertex) # type: ignore + color = self.vertex_color[vertex] # type: ignore - """ - self.edge_text = text - labels = [] - for edge in self.edge_text: - u, v = edge - labels.append( - { - "pos": centroid_points([self.vertex_xyz[u], self.vertex_xyz[v]]), - "name": f"{self.mesh.name}.edgelabel.{u}-{v}", - "text": self.edge_text[edge], - "color": self.edge_color[edge], - } - ) - return compas_blender.draw_texts(labels, collection=self.edgelabelcollection) - - def draw_facelabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: - """Draw labels for a selection of faces. + sphere = Sphere.from_point_and_radius(self.vertex_xyz[vertex], radius[vertex]) + geometry = conversions.sphere_to_blender_mesh(sphere, name=name) + + obj = self.create_object(geometry, name=name) + self.update_object(obj, color=color, collection=collection) # type: ignore + + objects.append(obj) + + return objects + + def draw_pipes( + self, + radius: Dict[Tuple[int, int], float], + color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, + ) -> list[bpy.types.Object]: + """Draw pipes around the edges of the mesh. Parameters ---------- - text : dict[int, str], optional - A dictionary of face labels as face-text pairs. - The default value is None, in which case every face will be labeled with its identifier. + radius : dict[tuple[int, int], float] + The radius per edge. + color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional + The color of the pipes. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ - self.face_text = text - labels = [] - for face in self.face_text: - labels.append( - { - "pos": centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]), - "name": "{}.facelabel.{}".format(self.mesh.name, face), - "text": self.face_text[face], - "color": self.face_color[face], - } - ) - return compas_blender.draw_texts(labels, collection=self.collection) + objects = [] + + self.edge_color = color + + for u, v in radius: + name = "{}.edge.{}-{}.pipe".format(self.mesh.name, u, v) # type: ignore + color = self.edge_color[u, v] # type: ignore + + line = Line(self.vertex_xyz[u], self.vertex_xyz[v]) + cylinder = Cylinder.from_line_and_radius(line, radius[u, v]) # type: ignore + geometry = conversions.cylinder_to_blender_mesh(cylinder) + + obj = self.create_object(geometry, name=name) + self.update_object(obj, color=color, collection=collection) # type: ignore + + objects.append(obj) + + return objects diff --git a/src/compas_blender/artists/networkartist.py b/src/compas_blender/artists/networkartist.py index 51a5d33f340..24268ea5868 100644 --- a/src/compas_blender/artists/networkartist.py +++ b/src/compas_blender/artists/networkartist.py @@ -5,103 +5,33 @@ from typing import Tuple from typing import Union -import bpy -from functools import partial +import bpy # type: ignore import compas_blender from compas.datastructures import Network -from compas.geometry import centroid_points -from compas.utilities import color_to_colordict -from compas.artists import NetworkArtist from compas.colors import Color +from compas.geometry import Line + +from compas.artists import NetworkArtist as BaseArtist from .artist import BlenderArtist -colordict = partial(color_to_colordict, colorformat="rgb", normalize=True) +from compas_blender import conversions -class NetworkArtist(BlenderArtist, NetworkArtist): +class NetworkArtist(BlenderArtist, BaseArtist): """Artist for drawing network data structures in Blender. Parameters ---------- network : :class:`~compas.datastructures.Network` A COMPAS network. - collection : str | :blender:`bpy.types.Collection` - The name of the collection the object belongs to. - - Attributes - ---------- - nodecollection : :blender:`bpy.types.Collection` - The collection containing the nodes. - edgecollection : :blender:`bpy.types.Collection` - The collection containing the edges. - nodelabelcollection : :blender:`bpy.types.Collection` - The collection containing the node labels. - edgelabelcollection : :blender:`bpy.types.Collection` - The collection containing the edge labels. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - import compas - from compas.datastructures import Network - from compas_blender.artists import NetworkArtist - - network = Network.from_obj(compas.get('lines.obj')) - - artist = NetworkArtist(network) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - import compas - from compas.datastructures import Network - from compas.artists import Artist - - network = Network.from_obj(compas.get('lines.obj')) - - artist = Artist(network) - artist.draw() """ - 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) - - @property - def nodecollection(self) -> bpy.types.Collection: - if not self._nodecollection: - self._nodecollection = compas_blender.create_collection("Nodes", parent=self.collection) - return self._nodecollection - - @property - def edgecollection(self) -> bpy.types.Collection: - if not self._edgecollection: - self._edgecollection = compas_blender.create_collection("Edges", parent=self.collection) - return self._edgecollection - - @property - def nodelabelcollection(self) -> bpy.types.Collection: - if not self._nodelabelcollection: - self._nodelabelcollection = compas_blender.create_collection("NodeLabels", parent=self.collection) - return self._nodelabelcollection - - @property - def edgelabelcollection(self) -> bpy.types.Collection: - if not self._edgelabelcollection: - self._edgelabelcollection = compas_blender.create_collection("EdgeLabels", parent=self.collection) - return self._edgelabelcollection + def __init__(self, network: Network, **kwargs: Any): + super().__init__(network=network, **kwargs) + self.nodeobjects = [] + self.edgeobjects = [] # ========================================================================== # clear @@ -115,7 +45,7 @@ def clear_nodes(self): None """ - compas_blender.delete_objects(self.nodecollection.objects) + compas_blender.delete_objects(self.nodeobjects) def clear_edges(self): """Clear all objects contained in the edge collection. @@ -125,27 +55,27 @@ def clear_edges(self): None """ - compas_blender.delete_objects(self.edgecollection.objects) + compas_blender.delete_objects(self.edgeobjects) - def clear_nodelabels(self): - """Clear all objects contained in the nodelabel collection. + # def clear_nodelabels(self): + # """Clear all objects contained in the nodelabel collection. - Returns - ------- - None + # Returns + # ------- + # None - """ - compas_blender.delete_objects(self.nodelabelcollection.objects) + # """ + # compas_blender.delete_objects(self.nodelabelcollection.objects) - def clear_edgelabels(self): - """Clear all objects contained in the edgelabel collection. + # def clear_edgelabels(self): + # """Clear all objects contained in the edgelabel collection. - Returns - ------- - None + # Returns + # ------- + # None - """ - compas_blender.delete_objects(self.edgelabelcollection.objects) + # """ + # compas_blender.delete_objects(self.edgelabelcollection.objects) # ========================================================================== # draw @@ -155,8 +85,8 @@ def draw( self, nodes: Optional[List[int]] = None, edges: Optional[Tuple[int, int]] = None, - nodecolor: Optional[Union[str, Color, Dict[int, Color]]] = None, - edgecolor: Optional[Union[str, Color, Dict[int, Color]]] = None, + nodecolor: Optional[Union[Color, Dict[int, Color]]] = None, + edgecolor: Optional[Union[Color, Dict[Tuple[int, int], Color]]] = None, ) -> None: """Draw the network. @@ -179,15 +109,17 @@ def draw( """ self.clear() - if self.show_nodes: - self.draw_nodes(nodes=nodes, color=nodecolor) - if self.show_edges: - self.draw_edges(edges=edges, color=edgecolor) + self.draw_nodes(nodes=nodes, color=nodecolor) + self.draw_edges(edges=edges, color=edgecolor) def draw_nodes( self, nodes: Optional[List[int]] = None, color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, + radius: float = 0.05, + u: int = 16, + v: int = 16, ) -> List[bpy.types.Object]: """Draw a selection of nodes. @@ -198,31 +130,37 @@ def draw_nodes( Default is None, in which case all nodes are drawn. color : :class:`~compas.colors.Color` | dict[hashable, :class:`~compas.colors.Color`], optional The color specification for the nodes. - The default color of nodes is :attr:`default_nodecolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ + objects = [] + self.node_color = color - nodes = nodes or self.nodes - points = [] - for node in nodes: - points.append( - { - "pos": self.node_xyz[node], - "name": f"{self.network.name}.node.{node}", - "color": self.node_color[node], - "radius": 0.05, - } - ) - return compas_blender.draw_points(points, self.nodecollection) + + for node in nodes or self.network.nodes(): # type: ignore + name = f"{self.network.name}.node.{node}" # type: ignore + color = self.node_color[node] # type: ignore + point = self.node_xyz[node] # type: ignore + + # there is no such thing as a sphere data block + bpy.ops.mesh.primitive_uv_sphere_add(location=point, radius=radius, segments=u, ring_count=v) + obj = bpy.context.object + self.objects.append(obj) + self.update_object(obj, name=name, color=color, collection=collection) + objects.append(obj) + + return objects def draw_edges( self, edges: Optional[Tuple[int, int]] = None, - color: Optional[Union[Color, Dict[int, Color]]] = None, + color: Optional[Union[Color, Dict[Tuple[int, int], Color]]] = None, + collection: Optional[str] = None, ) -> List[bpy.types.Object]: """Draw a selection of edges. @@ -233,80 +171,88 @@ def draw_edges( The default is None, in which case all edges are drawn. color : :class:`~compas.colors.Color` | dict[tuple[hashable, hashable], :class:`~compas.colors.Color`], optional The color specification for the edges. - The default color of edges is :attr:`default_edgecolor`. - - Returns - ------- - list[:blender:`bpy.types.Object`] - - """ - self.edge_color = color - edges = edges or self.edges - lines = [] - for edge in edges: - u, v = edge - lines.append( - { - "start": self.node_xyz[u], - "end": self.node_xyz[v], - "color": self.edge_color[edge], - "name": f"{self.network.name}.edge.{u}-{v}", - "width": self.edge_width[edge], - } - ) - return compas_blender.draw_lines(lines, self.edgecollection) - - def draw_nodelabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: - """Draw labels for a selection nodes. - - Parameters - ---------- - text : dict[hashable, str], optional - A dictionary of vertex labels as vertex-text pairs. - The default value is None, in which case every vertex will be labeled with its key. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ - self.node_text = text - labels = [] - for node in self.node_text: - labels.append( - { - "pos": self.node_xyz[node], - "name": f"{self.network.name}.nodelabel.{node}", - "text": self.node_text[node], - "color": self.node_color[node], - } - ) - return compas_blender.draw_texts(labels, collection=self.nodelabelcollection) - - def draw_edgelabels(self, text: Optional[Dict[Tuple[int, int], str]] = None) -> List[bpy.types.Object]: - """Draw labels for a selection of edges. - - Parameters - ---------- - text : dict[tuple[hashable, hashable], str], optional - A dictionary of edge labels as edge-text pairs. - The default value is None, in which case every edge will be labeled with its key. + objects = [] - Returns - ------- - list[:blender:`bpy.types.Object`] + self.edge_color = color - """ - self.edge_text = text - labels = [] - for edge in self.edge_text: - u, v = edge - labels.append( - { - "pos": centroid_points([self.node_xyz[u], self.node_xyz[v]]), - "name": f"{self.network.name}.edgelabel.{u}-{v}", - "text": self.edge_text[edge], - "color": self.edge_color[edge], - } - ) - return compas_blender.draw_texts(labels, collection=self.edgelabelcollection) + for u, v in edges or self.network.edges(): # type: ignore + name = f"{self.network.name}.edge.{u}-{v}" # type: ignore + color = self.edge_color[u, v] # type: ignore + curve = conversions.line_to_blender_curve(Line(self.node_xyz[u], self.node_xyz[v])) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) + objects.append(obj) + + return objects + + # ============================================================================= + # draw labels + # ============================================================================= + + # def draw_nodelabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: + # """Draw labels for a selection nodes. + + # Parameters + # ---------- + # text : dict[hashable, str], optional + # A dictionary of vertex labels as vertex-text pairs. + # The default value is None, in which case every vertex will be labeled with its key. + + # Returns + # ------- + # list[:blender:`bpy.types.Object`] + + # """ + # self.node_text = text + # labels = [] + # for node in self.node_text: + # labels.append( + # { + # "pos": self.node_xyz[node], + # "name": f"{self.network.name}.nodelabel.{node}", + # "text": self.node_text[node], + # "color": self.node_color[node], + # } + # ) + # return compas_blender.draw_texts(labels, collection=self.nodelabelcollection) + + # def draw_edgelabels(self, text: Optional[Dict[Tuple[int, int], str]] = None) -> List[bpy.types.Object]: + # """Draw labels for a selection of edges. + + # Parameters + # ---------- + # text : dict[tuple[hashable, hashable], str], optional + # A dictionary of edge labels as edge-text pairs. + # The default value is None, in which case every edge will be labeled with its key. + + # Returns + # ------- + # list[:blender:`bpy.types.Object`] + + # """ + # self.edge_text = text + # labels = [] + # for edge in self.edge_text: + # u, v = edge + # labels.append( + # { + # "pos": centroid_points([self.node_xyz[u], self.node_xyz[v]]), + # "name": f"{self.network.name}.edgelabel.{u}-{v}", + # "text": self.edge_text[edge], + # "color": self.edge_color[edge], + # } + # ) + # return compas_blender.draw_texts(labels, collection=self.edgelabelcollection) + + # ============================================================================= + # draw miscellaneous + # ============================================================================= diff --git a/src/compas_blender/artists/pointartist.py b/src/compas_blender/artists/pointartist.py index fa74f5b14f8..b9a1836d1cd 100644 --- a/src/compas_blender/artists/pointartist.py +++ b/src/compas_blender/artists/pointartist.py @@ -1,89 +1,70 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender -from compas.artists import PrimitiveArtist from compas.geometry import Point from compas.colors import Color + +from compas.artists import GeometryArtist from .artist import BlenderArtist -class PointArtist(BlenderArtist, PrimitiveArtist): +class PointArtist(BlenderArtist, GeometryArtist): """Artist for drawing points in Blender. Parameters ---------- point : :class:`~compas.geometry.Point` A COMPAS point. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Point - from compas_blender.artists import PointArtist - - point = Point(0, 0, 0) - - artist = PointArtist(point) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Point - from compas.artists import Artist - - point = Point(0, 0, 0) - - artist = Artist(point) - artist.draw() """ - def __init__( - self, - point: Point, - collection: Optional[Union[str, bpy.types.Collection]] = None, - **kwargs: Any, - ): - super().__init__(primitive=point, collection=collection or point.name, **kwargs) + def __init__(self, point: Point, **kwargs: Any): + super().__init__(geometry=point, **kwargs) - def draw(self, color: Optional[Color] = None) -> List[bpy.types.Object]: + def draw( + self, + color: Optional[Color] = None, + collection: Optional[str] = None, + radius: float = 0.01, + u: int = 16, + v: int = 16, + ) -> bpy.types.Object: """Draw the point. Parameters ---------- color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional Color of the point object. - The default color is :attr:`compas.artists.PrimitiveArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). + radius : float, optional + Radius of the point object. + u : int, optional + Number of faces in the "u" direction. + v : int, optional + Number of faces in the "v" direction. Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` """ + name = self.geometry.name color = Color.coerce(color) or self.color - points = [ - { - "pos": self.primitive, - "name": f"{self.primitive.name}", - "color": color, - "radius": 0.01, - } - ] - objects = compas_blender.draw_points(points, self.collection) - return objects + + bpy.ops.mesh.primitive_uv_sphere_add( + location=self.geometry, + radius=radius, + segments=u, + ring_count=v, + ) + + obj = bpy.context.object + self.objects.append(obj) + self.update_object(obj, name=name, color=color, collection=collection) + + return obj diff --git a/src/compas_blender/artists/pointcloudartist.py b/src/compas_blender/artists/pointcloudartist.py new file mode 100644 index 00000000000..4c28ecc437f --- /dev/null +++ b/src/compas_blender/artists/pointcloudartist.py @@ -0,0 +1,66 @@ +from typing import Any +from typing import Optional + +import bpy # type: ignore + +from compas.geometry import Point +from compas.colors import Color + +from compas.artists import GeometryArtist +from .artist import BlenderArtist + +from compas_blender import conversions + + +class PointcloudArtist(BlenderArtist, GeometryArtist): + """Artist for drawing pointclouds in Blender. + + Parameters + ---------- + pointcloud : :class:`~compas.geometry.Pointcloud` + A COMPAS point. + **kwargs : dict, optional + Additional keyword arguments. + + """ + + def __init__(self, point: Point, **kwargs: Any): + super().__init__(geometry=point, **kwargs) + + def draw( + self, + color: Optional[Color] = None, + collection: Optional[str] = None, + radius: float = 0.01, + u: int = 16, + v: int = 16, + ) -> bpy.types.Object: + """Draw the pointcloud. + + Parameters + ---------- + color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional + Color of the point object. + collection : str, optional + The name of the Blender scene collection containing the created object(s). + radius : float, optional + The radius of the spheres representing the vertices. + u : int, optional + Number of faces in the "u" direction of the spheres representing the vertices. + v : int, optional + Number of faces in the "v" direction of the spheres representing the vertices. + + Returns + ------- + :blender:`bpy.types.Object` + + """ + name = self.geometry.name + color = Color.coerce(color) or self.color + + mesh = conversions.pointcloud_to_blender(self.geometry, name=name, u=u, v=v, radius=radius) + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection) + + return obj diff --git a/src/compas_blender/artists/polygonartist.py b/src/compas_blender/artists/polygonartist.py index 17d5befba09..e5dc7bfe01e 100644 --- a/src/compas_blender/artists/polygonartist.py +++ b/src/compas_blender/artists/polygonartist.py @@ -1,64 +1,37 @@ from typing import Any from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender -from compas.artists import PrimitiveArtist from compas.geometry import Polygon from compas.colors import Color + +from compas_blender import conversions + +from compas.artists import GeometryArtist from .artist import BlenderArtist -class PolygonArtist(BlenderArtist, PrimitiveArtist): +class PolygonArtist(BlenderArtist, GeometryArtist): """Artist for drawing polygons in Blender. Parameters ---------- polygon : :class:`~compas.geometry.Polygon` A COMPAS polygon. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. - - Examples - -------- - .. code-block:: python - - from compas.geometry import Polygon - from compas_blender.artists import PolygonArtist - - polygon = Polygon.from_sides_and_radius_xy(5, 1) - - artist = PolygonArtist(polygon) - artist.draw() - - .. code-block:: python - - from compas.geometry import Polygon - from compas.artists import Artist - - polygon = Polygon.from_sides_and_radius_xy(5, 1) - - artist = Artist(polygon) - artist.draw() """ - def __init__(self, polygon: Polygon, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): - super().__init__(primitive=polygon, collection=collection or polygon.name, **kwargs) + def __init__(self, polygon: Polygon, **kwargs: Any): + super().__init__(geometry=polygon, **kwargs) def draw( self, color: Optional[Color] = None, - show_points: bool = False, - show_edges: bool = False, - show_face: bool = True, + collection: Optional[str] = None, ) -> List[bpy.types.Object]: """Draw the polygon. @@ -66,44 +39,20 @@ def draw( ---------- color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional The RGB color of the polygon. - The default color is :attr:`compas.artists.PrimitiveArtist.color`. - show_points : bool, optional - If True, draw the corner points of the polygon. - show_edges : bool, optional - If True, draw the edges of the polygon. - show_face : bool, optional - If True, draw the face of the polygon. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` """ + name = self.geometry.name color = Color.coerce(color) or self.color - objects = [] - if show_points: - points = [ - { - "pos": point, - "color": color, - "name": self.primitive.name, - "radius": 0.01, - } - for point in self.primitive.points - ] - objects += compas_blender.draw_points(points, collection=self.collection) - if show_edges: - lines = [ - {"start": a, "end": b, "color": color, "name": self.primitive.name} for a, b in self.primitive.lines - ] - objects += compas_blender.draw_lines(lines, collection=self.collection) - if show_face: - polygons = [ - { - "points": self.primitive.points, - "color": color, - "name": self.primitive.name, - } - ] - objects += compas_blender.draw_faces(polygons, collection=self.collection) - return objects + + vertices, faces = self.geometry.to_vertices_and_faces() + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=self.geometry.name) + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection) + + return obj diff --git a/src/compas_blender/artists/polyhedronartist.py b/src/compas_blender/artists/polyhedronartist.py index 8aa1780e4a4..da0fa10dd20 100644 --- a/src/compas_blender/artists/polyhedronartist.py +++ b/src/compas_blender/artists/polyhedronartist.py @@ -1,86 +1,59 @@ from typing import Any from typing import List from typing import Optional -from typing import Union -import bpy -import compas_blender +import bpy # type: ignore from compas.geometry import Polyhedron -from compas.artists import ShapeArtist from compas.colors import Color + +from compas.artists import GeometryArtist from .artist import BlenderArtist +from compas_blender import conversions + -class PolyhedronArtist(BlenderArtist, ShapeArtist): +class PolyhedronArtist(BlenderArtist, GeometryArtist): """Artist for drawing polyhedron shapes in Blender. Parameters ---------- polyhedron : :class:`~compas.geometry.Polyhedron` A COMPAS polyhedron. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.ShapeArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Polyhedron - from compas_blender.artists import PolyhedronArtist - - polyhedron = Polyhedron.from_platonicsolid(12) - - artist = PolyhedronArtist(polyhedron) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Polyhedron - from compas.artists import Artist - - polyhedron = Polyhedron.from_platonicsolid(12) - - artist = Artist(polyhedron) - artist.draw() """ - def __init__( - self, polyhedron: Polyhedron, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any - ): + def __init__(self, polyhedron: Polyhedron, **kwargs: Any): + super().__init__(geometry=polyhedron, **kwargs) - super().__init__(shape=polyhedron, collection=collection or polyhedron.name, **kwargs) - - def draw(self, color: Optional[Color] = None) -> List[bpy.types.Object]: + def draw( + self, color: Optional[Color] = None, collection: Optional[str] = None, show_wire: bool = True + ) -> List[bpy.types.Object]: """Draw the polyhedron associated with the artist. Parameters ---------- color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional The RGB color of the polyhedron. - The default color is :attr:`compas.artists.ShapeArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). + show_wire : bool, optional + Display the wireframe of the polyhedron. Returns ------- - list[:blender:`bpy.types.Object`] - The objects created in Blender. + :blender:`bpy.types.Object` + The object created in Blender. """ + name = self.geometry.name color = Color.coerce(color) or self.color - vertices, faces = self.shape.to_vertices_and_faces() - obj = compas_blender.draw_mesh( - vertices, - faces, - name=self.shape.name, - color=color, - collection=self.collection, - ) - return [obj] + + vertices, faces = self.geometry.to_vertices_and_faces() + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=self.geometry.name) + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + + return obj diff --git a/src/compas_blender/artists/polylineartist.py b/src/compas_blender/artists/polylineartist.py index a56971effbf..1ef1d54649a 100644 --- a/src/compas_blender/artists/polylineartist.py +++ b/src/compas_blender/artists/polylineartist.py @@ -1,105 +1,53 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender -from compas.artists import PrimitiveArtist from compas.geometry import Polyline from compas.colors import Color + +from compas.artists import GeometryArtist from .artist import BlenderArtist +from compas_blender import conversions + -class PolylineArtist(BlenderArtist, PrimitiveArtist): +class PolylineArtist(BlenderArtist, GeometryArtist): """Artist for drawing polylines in Blender. Parameters ---------- polyline : :class:`~compas.geometry.Polyline` A COMPAS polyline. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Polyline - from compas_blender.artists import PolylineArtist - - polyline = Polyline([[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]]) - - artist = PolylineArtist(polyline) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Polyline - from compas.artists import Artist - - polyline = Polyline([[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]]) - - artist = Artist(polyline) - artist.draw() """ - def __init__( - self, - polyline: Polyline, - collection: Optional[Union[str, bpy.types.Collection]] = None, - **kwargs: Any, - ): - super().__init__(primitive=polyline, collection=collection or polyline.name, **kwargs) + def __init__(self, polyline: Polyline, **kwargs: Any): + super().__init__(geometry=polyline, **kwargs) - def draw(self, color: Optional[Color] = None, show_points: Optional[bool] = False) -> List[bpy.types.Object]: + def draw(self, color: Optional[Color] = None, collection: Optional[str] = None) -> bpy.types.Object: """Draw the line. Parameters ---------- color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional The RGB color of the polyline. - The default color is :attr:`compas.artists.PrimitiveArtist.color`. - show_points : bool, optional - If True, draw the corner points of the polyline. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` """ + name = self.geometry.name color = Color.coerce(color) or self.color - lines = [ - { - "start": start, - "end": end, - "color": self.color, - "name": f"{self.primitive.name}", - } - for start, end in self.primitive.lines - ] - objects = compas_blender.draw_lines(lines, collection=self.collection) - - if show_points: - points = [ - { - "pos": point, - "name": f"{self.primitive.name}.point", - "color": color, - "radius": 0.01, - } - for point in self.primitive.points - ] - objects += compas_blender.draw_points(points, collection=self.collection) - return objects + curve = conversions.polyline_to_blender_curve(self.geometry, name=name) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) + + return obj diff --git a/src/compas_blender/artists/robotmodelartist.py b/src/compas_blender/artists/robotmodelartist.py index 0cfacfddd3c..78f2626955e 100644 --- a/src/compas_blender/artists/robotmodelartist.py +++ b/src/compas_blender/artists/robotmodelartist.py @@ -4,7 +4,7 @@ from typing import Any from typing import List -import bpy +import bpy # type: ignore import mathutils import compas_blender diff --git a/src/compas_blender/artists/sphereartist.py b/src/compas_blender/artists/sphereartist.py index 0292cde07f9..6cf199322d7 100644 --- a/src/compas_blender/artists/sphereartist.py +++ b/src/compas_blender/artists/sphereartist.py @@ -1,92 +1,73 @@ from typing import Optional from typing import Any from typing import List -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender from compas.geometry import Sphere -from compas.artists import ShapeArtist from compas.colors import Color + +from compas.artists import GeometryArtist from .artist import BlenderArtist +from compas_blender import conversions + -class SphereArtist(BlenderArtist, ShapeArtist): +class SphereArtist(BlenderArtist, GeometryArtist): """Artist for drawing sphere shapes in Blender. Parameters ---------- sphere : :class:`~compas.geometry.Sphere` A COMPAS sphere. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.ShapeArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Sphere - from compas_blender.artists import SphereArtist - - sphere = Sphere([0, 0, 0], 1) - - artist = SphereArtist(sphere) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Sphere - from compas.artists import Artist - - sphere = Sphere([0, 0, 0], 1) - - artist = Artist(sphere) - artist.draw() """ - def __init__(self, sphere: Sphere, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): - - super().__init__(shape=sphere, collection=collection or sphere.name, **kwargs) - - def draw(self, color: Optional[Color] = None, u: int = None, v: int = None) -> List[bpy.types.Object]: + def __init__(self, sphere: Sphere, **kwargs: Any): + super().__init__(geometry=sphere, **kwargs) + + def draw( + self, + color: Optional[Color] = None, + collection: Optional[str] = None, + u: int = 16, + v: int = 16, + show_wire: bool = False, + shade_smooth: bool = True, + ) -> List[bpy.types.Object]: """Draw the sphere associated with the artist. Parameters ---------- color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional The RGB color of the sphere. - The default color is :attr:`compas.artists.ShapeArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). 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_wire : bool, optional + Display the wireframe of the sphere. + shade_smooth : bool, optional + Display smooth shading on the sphere. Returns ------- list The objects created in Blender. """ - u = u or self.u - v = v or self.v + name = self.geometry.name color = Color.coerce(color) or self.color - vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) - obj = compas_blender.draw_mesh( - vertices, - faces, - name=self.shape.name, - color=color, - collection=self.collection, - ) - return [obj] + + vertices, faces = self.geometry.to_vertices_and_faces(u=u, v=v) + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=self.geometry.name) + if shade_smooth: + mesh.shade_smooth() + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + + return obj diff --git a/src/compas_blender/artists/surfaceartist.py b/src/compas_blender/artists/surfaceartist.py index 3ac245784a9..8a40f349d26 100644 --- a/src/compas_blender/artists/surfaceartist.py +++ b/src/compas_blender/artists/surfaceartist.py @@ -1,77 +1,53 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender -from compas.artists import SurfaceArtist from compas.geometry import Surface from compas.colors import Color + +from compas.artists import GeometryArtist from compas_blender.artists import BlenderArtist +from compas_blender import conversions + -class SurfaceArtist(BlenderArtist, SurfaceArtist): +class SurfaceArtist(BlenderArtist, GeometryArtist): """Artist for drawing surfaces in Blender. Parameters ---------- surface : :class:`~compas.geometry.Surface` A COMPAS surface. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import NurbsSurface - from compas_blender.artists import SurfaceArtist - - surface = NurbsSurface([[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]]) - - artist = SurfaceArtist(surface) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import NurbsSurface - from compas.artists import Artist - - surface = NurbsSurface([[0, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1]]) - - artist = Artist(surface) - artist.draw() """ - def __init__(self, surface: Surface, collection: Optional[Union[str, bpy.types.Collection]] = None, **kwargs: Any): + def __init__(self, surface: Surface, **kwargs: Any): + super().__init__(geometry=surface, **kwargs) - super().__init__(surface=surface, collection=collection or surface.name, **kwargs) - - def draw(self, color: Optional[Color] = None) -> List[bpy.types.Object]: + def draw(self, color: Optional[Color] = None, collection: Optional[str] = None) -> bpy.types.Object: """Draw the surface. Parameters ---------- color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional The RGB color of the surface. - The default color is :attr:`compas.artists.SurfaceArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` """ + name = self.geometry.name color = Color.coerce(color) or self.color - surfaces = [{"surface": self.surface, "color": color, "name": self.surface.name}] - return compas_blender.draw_surfaces(surfaces, collection=self.collection) + + surface = conversions.nurbssurface_to_blender_surface(self.geometry) + + obj = self.create_object(surface, name=name) + self.update_object(obj, color=color, collection=collection) + + return obj diff --git a/src/compas_blender/artists/torusartist.py b/src/compas_blender/artists/torusartist.py index b34f2f56cda..c16a3963154 100644 --- a/src/compas_blender/artists/torusartist.py +++ b/src/compas_blender/artists/torusartist.py @@ -1,98 +1,72 @@ from typing import Optional from typing import Any -from typing import List -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender from compas.geometry import Torus -from compas.artists import ShapeArtist from compas.colors import Color + +from compas.artists import GeometryArtist from .artist import BlenderArtist +from compas_blender import conversions + -class TorusArtist(BlenderArtist, ShapeArtist): +class TorusArtist(BlenderArtist, GeometryArtist): """Artist for drawing torus shapes in Blender. Parameters ---------- torus : :class:`~compas.geometry.Torus` A COMPAS torus. - collection: str | :blender:`bpy.types.Collection` - The name of the collection the object belongs to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.ShapeArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Plane, Torus - from compas_blender.artists import TorusArtist - - torus = Torus(Plane([0, 0, 0], [0, 0, 1]), 1.0, 0.3) - - artist = TorusArtist(torus) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Plane, Torus - from compas.artists import Artist - - torus = Torus(Plane([0, 0, 0], [0, 0, 1]), 1.0, 0.3) - - artist = Artist(torus) - artist.draw() """ - 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 __init__(self, torus: Torus, **kwargs: Any): + super().__init__(geometry=torus, **kwargs) def draw( self, color: Optional[Color] = None, - u: Optional[int] = None, - v: Optional[int] = None, - ) -> List[bpy.types.Object]: + collection: Optional[str] = None, + u: Optional[int] = 16, + v: Optional[int] = 16, + show_wire: bool = False, + shade_smooth: bool = True, + ) -> bpy.types.Object: """Draw the torus associated with the artist. Parameters ---------- color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional The RGB color of the torus. - The default color is :attr:`compas.artists.ShapeArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). u : int, optional Number of faces in the "u" direction. - Default is :attr:`TorusArtist.u`. v : int, optional Number of faces in the "v" direction. - Default is :attr:`TorusArtist.v`. + show_wire : bool, optional + Display the wireframe of the torus. + shade_smooth : bool, optional + Display smooth shading on the torus. Returns ------- - list - The objects created in Blender. + :blender:`bpy.types.Curve` """ - u = u or self.u - v = v or self.v + name = self.geometry.name color = Color.coerce(color) or self.color - vertices, faces = self.shape.to_vertices_and_faces(u=u, v=v) - obj = compas_blender.draw_mesh( - vertices, - faces, - name=self.shape.name, - color=color, - collection=self.collection, - ) - return [obj] + + vertices, faces = self.geometry.to_vertices_and_faces(u=u, v=v) + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=self.geometry.name) + if shade_smooth: + mesh.shade_smooth() + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + + return obj diff --git a/src/compas_blender/artists/vectorartist.py b/src/compas_blender/artists/vectorartist.py index d637f20da86..c8f512ddc3f 100644 --- a/src/compas_blender/artists/vectorartist.py +++ b/src/compas_blender/artists/vectorartist.py @@ -1,113 +1,68 @@ from typing import Any -from typing import List from typing import Optional -from typing import Union -import bpy +import bpy # type: ignore -import compas_blender -from compas.artists import PrimitiveArtist from compas.geometry import Point from compas.geometry import Vector +from compas.geometry import Line from compas.colors import Color + +from compas.artists import GeometryArtist from .artist import BlenderArtist +from compas_blender import conversions + -class VectorArtist(BlenderArtist, PrimitiveArtist): +class VectorArtist(BlenderArtist, GeometryArtist): """Artist for drawing vectors in Blender. Parameters ---------- primitive : :class:`~compas.geometry.Vector` A COMPAS vector. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.PrimitiveArtist`. - - Examples - -------- - Use the Blender artist explicitly. - - .. code-block:: python - - from compas.geometry import Vector - from compas_blender.artists import VectorArtist - - vector = Vector(1, 1, 1) - - artist = VectorArtist(vector) - artist.draw() - - Or, use the artist through the plugin mechanism. - - .. code-block:: python - - from compas.geometry import Vector - from compas.artists import Artist - - vector = Vector(1, 1, 1) - - artist = Artist(vector) - artist.draw() """ - def __init__( - self, - vector: Vector, - collection: Optional[Union[str, bpy.types.Collection]] = None, - **kwargs: Any, - ): - super().__init__(primitive=vector, collection=collection or vector.name, **kwargs) + def __init__(self, vector: Vector, **kwargs: Any): + super().__init__(geometry=vector, **kwargs) def draw( self, color: Optional[Color] = None, + collection: Optional[str] = None, point: Optional[Point] = None, - show_point: Optional[bool] = False, - ) -> List[bpy.types.Object]: + ) -> bpy.types.Object: """Draw the vector. Parameters ---------- color : tuple[float, float, float] | tuple[int, int, int] | :class:`~compas.colors.Color`, optional The RGB color of the vector. - The default color is :attr:`compas.artists.PrimitiveArtist.color`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). point : [float, float, float] | :class:`~compas.geometry.Point`, optional Point of application of the vector. Default is ``Point(0, 0, 0)``. - show_point : bool, optional - If True, draw the point of application of the vector. Returns ------- - list[:blender:`bpy.types.Object`] + :blender:`bpy.types.Object` """ - point = point or (0.0, 0.0, 0.0) - start = Point(*point) - end = start + self.primitive + name = self.geometry.name color = Color.coerce(color) or self.color - lines = [ - { - "start": start, - "end": end, - "color": color, - "name": f"{self.primitive.name}", - }, - ] - objects = compas_blender.draw_lines(lines, self.collection) - if show_point: - points = [ - { - "pos": start, - "name": f"{self.primitive.name}.origin", - "color": (1.0, 1.0, 1.0), - "radius": 0.01, - } - ] - objects += compas_blender.draw_points(points, self.collection) - return objects + + point = point or (0.0, 0.0, 0.0) # type: ignore + start = Point(*point) # type: ignore + end = start + self.geometry + line = Line(start, end) + + curve = conversions.line_to_blender_curve(line) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) + + return obj diff --git a/src/compas_blender/artists/volmeshartist.py b/src/compas_blender/artists/volmeshartist.py index 8e76fc55e73..b40b87cb437 100644 --- a/src/compas_blender/artists/volmeshartist.py +++ b/src/compas_blender/artists/volmeshartist.py @@ -5,110 +5,47 @@ from typing import Dict from typing import Tuple -import bpy +import bpy # type: ignore import compas_blender from compas.geometry import add_vectors from compas.geometry import scale_vector from compas.geometry import centroid_points - -from compas.artists import VolMeshArtist +from compas.geometry import Line from compas.colors import Color + +from compas.artists import VolMeshArtist as BaseArtist from .artist import BlenderArtist +from compas_blender import conversions -class VolMeshArtist(BlenderArtist, VolMeshArtist): + +class VolMeshArtist(BlenderArtist, BaseArtist): """An artist for drawing volumetric mesh data structures in Blender. Parameters ---------- volmesh : :class:`~compas.datastructures.VolMesh` The volmesh data structure. - collection : str | :blender:`bpy.types.Collection` - The Blender scene collection the object(s) created by this artist belong to. **kwargs : dict, optional Additional keyword arguments. - For more info, - see :class:`~compas_blender.artists.BlenderArtist` and :class:`~compas.artists.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) - - @property - def vertexcollection(self) -> bpy.types.Collection: - if not self._vertexcollection: - self._vertexcollection = compas_blender.create_collection("Vertices", parent=self.collection) - return self._vertexcollection - - @property - def edgecollection(self) -> bpy.types.Collection: - if not self._edgecollection: - self._edgecollection = compas_blender.create_collection("Edges", parent=self.collection) - return self._edgecollection - - @property - def facecollection(self) -> bpy.types.Collection: - if not self._facecollection: - self._facecollection = compas_blender.create_collection("Faces", parent=self.collection) - return self._facecollection - - @property - def cellcollection(self) -> bpy.types.Collection: - if not self._cellcollection: - self._cellcollection = compas_blender.create_collection("Cells", parent=self.collection) - return self._cellcollection - - @property - def vertexnormalcollection(self) -> bpy.types.Collection: - if not self._vertexnormalcollection: - self._vertexnormalcollection = compas_blender.create_collection("VertexNormals", parent=self.collection) - return self._vertexnormalcollection - - @property - def facenormalcollection(self) -> bpy.types.Collection: - if not self._facenormalcollection: - self._facenormalcollection = compas_blender.create_collection("FaceNormals", parent=self.collection) - return self._facenormalcollection - - @property - def vertexlabelcollection(self) -> bpy.types.Collection: - if not self._vertexlabelcollection: - self._vertexlabelcollection = compas_blender.create_collection("VertexLabels", parent=self.collection) - return self._vertexlabelcollection - - @property - def edgelabelcollection(self) -> bpy.types.Collection: - if not self._edgelabelcollection: - self._edgelabelcollection = compas_blender.create_collection("EdgeLabels", parent=self.collection) - return self._edgelabelcollection - - @property - def facelabelcollection(self) -> bpy.types.Collection: - if not self._facelabelcollection: - self._facelabelcollection = compas_blender.create_collection("FaceLabels", parent=self.collection) - return self._facelabelcollection - - @property - def celllabelcollection(self) -> bpy.types.Collection: - if not self._celllabelcollection: - self._celllabelcollection = compas_blender.create_collection("CellLabels", parent=self.collection) - return self._celllabelcollection + def __init__(self, volmesh, **kwargs: Any): + super().__init__(volmesh=volmesh, **kwargs) + self.vertexobjects = [] + self.edgeobjects = [] + self.faceobjects = [] + self.cellobjects = [] # ========================================================================== # clear # ========================================================================== def clear(self): - compas_blender.delete_objects(self.collection.objects) + compas_blender.delete_objects(self.objects) def clear_vertices(self): """Clear the objects contained in the vertex collection (``self.vertexcollection``). @@ -118,7 +55,7 @@ def clear_vertices(self): None """ - compas_blender.delete_objects(self.vertexcollection.objects) + compas_blender.delete_objects(self.vertexobjects) def clear_edges(self): """Clear the objects contained in the edge collection (``self.edgecollection``). @@ -128,7 +65,7 @@ def clear_edges(self): None """ - compas_blender.delete_objects(self.edgecollection.objects) + compas_blender.delete_objects(self.edgeobjects) def clear_faces(self): """Clear the objects contained in the face collection (``self.facecollection``). @@ -138,7 +75,7 @@ def clear_faces(self): None """ - compas_blender.delete_objects(self.facecollection.objects) + compas_blender.delete_objects(self.faceobjects) def clear_cells(self): """Clear the objects contained in the cell collection (``self.cellcollection``). @@ -148,15 +85,15 @@ def clear_cells(self): None """ - compas_blender.delete_objects(self.facecollection.objects) - - # clear the labels + compas_blender.delete_objects(self.cellobjects) # ========================================================================== # draw # ========================================================================== - def draw(self, cells: Optional[List[int]] = None, color: Optional[Color] = None) -> List[bpy.types.Object]: + def draw( + self, cells: Optional[List[int]] = None, color: Optional[Color] = None, collection: Optional[str] = None + ) -> List[bpy.types.Object]: """Draw a selection of cells. Parameters @@ -167,18 +104,24 @@ def draw(self, cells: Optional[List[int]] = None, color: Optional[Color] = None) color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the cells. The default color is :attr:`VolMeshArtist.default_cellcolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ - return self.draw_cells(cells=cells, color=color) + return self.draw_cells(cells=cells, color=color, collection=collection) def draw_vertices( self, vertices: Optional[List[int]] = None, color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, + radius: float = 0.01, + u: int = 16, + v: int = 16, ) -> List[bpy.types.Object]: """Draw a selection of vertices. @@ -189,31 +132,43 @@ def draw_vertices( Default is None, in which case all vertices are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color specification for the vertices. - The default color of vertices is :attr:`default_vertexcolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). + radius : float, optional + The radius of the spheres representing the vertices. + u : int, optional + Number of faces in the "u" direction of the spheres representing the vertices. + v : int, optional + Number of faces in the "v" direction of the spheres representing the vertices. Returns ------- list[:blender:`bpy.types.Object`] """ + objects = [] + self.vertex_color = color - vertices = vertices or self.vertices - points = [] - for vertex in vertices: - points.append( - { - "pos": self.vertex_xyz[vertex], - "name": f"{self.mesh.name}.vertex.{vertex}", - "color": self.vertex_color[vertex], - "radius": 0.01, - } - ) - return compas_blender.draw_points(points, self.vertexcollection) + + for vertex in vertices or self.volmesh.vertices(): # type: ignore + name = f"{self.volmesh.name}.vertex.{vertex}" # type: ignore + color = self.vertex_color[vertex] # type: ignore + point = self.vertex_xyz[vertex] + + # there is no such thing as a sphere data block + bpy.ops.mesh.primitive_uv_sphere_add(location=point, radius=radius, segments=u, ring_count=v) + obj = bpy.context.object + self.objects.append(obj) + self.update_object(obj, name=name, color=color, collection=collection) # type: ignore + objects.append(obj) + + return objects def draw_edges( self, edges: Optional[List[Tuple[int, int]]] = None, color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, ) -> List[bpy.types.Object]: """Draw a selection of edges. @@ -224,32 +179,35 @@ def draw_edges( The default is None, in which case all edges are drawn. color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional The color specification for the edges. - The default color of edges is :attr:`default_edgecolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ + objects = [] + self.edge_color = color - edges = edges or self.edges - lines = [] - for edge in edges: - u, v = edge - lines.append( - { - "start": self.vertex_xyz[u], - "end": self.vertex_xyz[v], - "color": self.edge_color[edge], - "name": f"{self.mesh.name}.edge.{u}-{v}", - } - ) - return compas_blender.draw_lines(lines, self.edgecollection) + + for u, v in edges or self.volmesh.edges(): # type: ignore + name = f"{self.volmesh.name}.edge.{u}-{v}" # type: ignore + color = self.edge_color[u, v] # type: ignore + curve = conversions.line_to_blender_curve(Line(self.vertex_xyz[u], self.vertex_xyz[v])) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) # type: ignore + objects.append(obj) + + return objects def draw_faces( self, faces: Optional[List[int]] = None, color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, + show_wire: bool = True, ) -> List[bpy.types.Object]: """Draw a selection of faces. @@ -260,30 +218,38 @@ def draw_faces( The default is None, in which case all faces are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color specification for the faces. - Th default color of faces is :attr:`default_facecolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). + show_wire : bool, optional + Display the wireframe of the faces. Returns ------- list[:blender:`bpy.types.Object`] """ + objects = [] + 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.volmesh.face_vertices(face)], - "name": f"{self.volmesh.name}.face.{face}", - "color": self.face_color[face], - } - ) - return compas_blender.draw_faces(facets, self.facecollection) + + for face in faces or self.volmesh.faces(): # type: ignore + name = f"{self.volmesh.name}.face.{face}" # type: ignore + color = self.face_color[face] # type: ignore + points = [self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)] # type: ignore + mesh = conversions.polygon_to_blender_mesh(points, name=name) # type: ignore + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) # type: ignore + objects.append(obj) + + return objects def draw_cells( self, cells: Optional[List[int]] = None, color: Optional[Union[Color, Dict[int, Color]]] = None, + collection: Optional[str] = None, + show_wire: bool = True, ) -> List[bpy.types.Object]: """Draw a selection of cells. @@ -294,32 +260,39 @@ def draw_cells( The default is None, in which case all cells are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the cells. - The default color is :attr:`VolMeshArtist.default_cellcolor`. + collection : str, optional + The name of the Blender scene collection containing the created object(s). + show_wire : bool, optional + Display the wireframe of the cells. Returns ------- list[:blender:`bpy.types.Object`] """ + objects = [] + self.cell_color = color - cells = cells or self.cells - vertex_xyz = self.vertex_xyz - meshes = [] - for cell in cells: - vertices = self.volmesh.cell_vertices(cell) - faces = self.volmesh.cell_faces(cell) + + for cell in cells or self.volmesh.cells(): # type: ignore + name = f"{self.volmesh.name}.cell.{cell}" # type: ignore + color = self.cell_color[cell] # type: ignore + + vertices = self.volmesh.cell_vertices(cell) # type: ignore + faces = self.volmesh.cell_faces(cell) # type: ignore vertex_index = dict((vertex, index) for index, vertex in enumerate(vertices)) - vertices = [vertex_xyz[vertex] for vertex in vertices] - faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] - obj = compas_blender.draw_mesh( - vertices, - faces, - name=f"{self.volmesh.name}.cell.{cell}", - color=self.cell_color[cell], - collection=self.cellcollection, - ) - meshes.append(obj) - return meshes + + vertices = [self.vertex_xyz[vertex] for vertex in vertices] + faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] # type: ignore + + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=name) # type: ignore + + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=collection, show_wire=show_wire) # type: ignore + + objects.append(obj) + + return objects # ========================================================================== # draw normals @@ -330,6 +303,7 @@ def draw_vertexnormals( vertices: Optional[List[int]] = None, color: Color = Color.green(), scale: float = 1.0, + collection: Optional[str] = None, ) -> List[bpy.types.Object]: """Draw the normals at the vertices of the mesh. @@ -342,33 +316,40 @@ def draw_vertexnormals( The color specification of the normal vectors. scale : float, optional Scale factor for the vertex normals. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ - vertices = vertices or self.vertices - lines = [] - for vertex in vertices: + objects = [] + + color = Color.coerce(color) # type: ignore + + for vertex in vertices or self.volmesh.vertices(): # type: ignore + name = f"{self.volmesh.name}.vertex.{vertex}.normal" # type: ignore + a = self.vertex_xyz[vertex] - n = self.mesh.vertex_normal(vertex) + n = self.volmesh.vertex_normal(vertex) # type: ignore b = add_vectors(a, scale_vector(n, scale)) - lines.append( - { - "start": a, - "end": b, - "color": color, - "name": f"{self.mesh.name}.vertexnormal.{vertex}", - } - ) - return compas_blender.draw_lines(lines, collection=self.vertexnormalcollection) + + curve = conversions.line_to_blender_curve(Line(a, b)) + + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) + + objects.append(obj) + + return objects def draw_facenormals( self, faces: Optional[List[List[int]]] = None, color: Color = Color.cyan(), scale: float = 1.0, + collection: Optional[str] = None, ) -> List[bpy.types.Object]: """Draw the normals of the faces. @@ -381,110 +362,116 @@ def draw_facenormals( The color specification of the normal vectors. scale : float, optional Scale factor for the face normals. + collection : str, optional + The name of the Blender scene collection containing the created object(s). Returns ------- list[:blender:`bpy.types.Object`] """ - faces = faces or self.faces - lines = [] - for face in faces: - 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( - { - "start": a, - "end": b, - "name": f"{self.mesh.name}.facenormal.{face}", - "color": color, - } - ) - return compas_blender.draw_lines(lines, collection=self.facenormalcollection) + objects = [] - # ========================================================================== - # draw labels - # ========================================================================== + color = Color.coerce(color) # type: ignore - def draw_vertexlabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: - """Draw labels for a selection vertices. + for face in faces or self.volmesh.faces(): # type: ignore + name = f"{self.volmesh.name}.face.{face}.normal" # type: ignore - Parameters - ---------- - text : dict[int, str], optional - A dictionary of vertex labels as vertex-text pairs. - The default value is None, in which case every vertex will be labeled with its identifier. + a = centroid_points([self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)]) # type: ignore + n = self.volmesh.face_normal(face) # type: ignore + b = add_vectors(a, scale_vector(n, scale)) - Returns - ------- - list[:blender:`bpy.types.Object`] + curve = conversions.line_to_blender_curve(Line(a, b)) - """ - self.vertex_text = text - labels = [] - for vertex in self.vertex_text: - labels.append( - { - "pos": self.vertex_xyz[vertex], - "name": f"{self.mesh.name}.vertexlabel.{vertex}", - "text": self.vertex_text[vertex], - "color": self.vertex_color[vertex], - } - ) - return compas_blender.draw_texts(labels, collection=self.vertexlabelcollection) - - def draw_edgelabels(self, text: Optional[Dict[Tuple[int, int], str]] = None) -> List[bpy.types.Object]: - """Draw labels for a selection of edges. + obj = self.create_object(curve, name=name) + self.update_object(obj, color=color, collection=collection) - Parameters - ---------- - text : dict[tuple[int, int], str], optional - A dictionary of edge labels as edge-text pairs. - The default value is None, in which case every edge will be labeled with its identifier. + objects.append(obj) - Returns - ------- - list[:blender:`bpy.types.Object`] - - """ - self.edge_text = text - labels = [] - for edge in self.edge_text: - u, v = edge - labels.append( - { - "pos": centroid_points([self.vertex_xyz[u], self.vertex_xyz[v]]), - "name": f"{self.mesh.name}.edgelabel.{u}-{v}", - "text": self.edge_text[edge], - "color": self.edge_color[edge], - } - ) - return compas_blender.draw_texts(labels, collection=self.edgelabelcollection) - - def draw_facelabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: - """Draw labels for a selection of faces. - - Parameters - ---------- - text : dict[int, str], optional - A dictionary of face labels as face-text pairs. - The default value is None, in which case every face will be labeled with its identifier. + return objects - Returns - ------- - list[:blender:`bpy.types.Object`] + # ========================================================================== + # draw labels + # ========================================================================== - """ - self.face_text = text - labels = [] - for face in self.face_text: - labels.append( - { - "pos": centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]), - "name": "{}.facelabel.{}".format(self.mesh.name, face), - "text": self.face_text[face], - "color": self.face_color[face], - } - ) - return compas_blender.draw_texts(labels, collection=self.collection) + # def draw_vertexlabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: + # """Draw labels for a selection vertices. + + # Parameters + # ---------- + # text : dict[int, str], optional + # A dictionary of vertex labels as vertex-text pairs. + # The default value is None, in which case every vertex will be labeled with its identifier. + + # Returns + # ------- + # list[:blender:`bpy.types.Object`] + + # """ + # self.vertex_text = text + # labels = [] + # for vertex in self.vertex_text: + # labels.append( + # { + # "pos": self.vertex_xyz[vertex], + # "name": f"{self.volmesh.name}.vertexlabel.{vertex}", + # "text": self.vertex_text[vertex], + # "color": self.vertex_color[vertex], + # } + # ) + # return compas_blender.draw_texts(labels, collection=self.vertexlabelcollection) + + # def draw_edgelabels(self, text: Optional[Dict[Tuple[int, int], str]] = None) -> List[bpy.types.Object]: + # """Draw labels for a selection of edges. + + # Parameters + # ---------- + # text : dict[tuple[int, int], str], optional + # A dictionary of edge labels as edge-text pairs. + # The default value is None, in which case every edge will be labeled with its identifier. + + # Returns + # ------- + # list[:blender:`bpy.types.Object`] + + # """ + # self.edge_text = text + # labels = [] + # for edge in self.edge_text: + # u, v = edge + # labels.append( + # { + # "pos": centroid_points([self.vertex_xyz[u], self.vertex_xyz[v]]), + # "name": f"{self.volmesh.name}.edgelabel.{u}-{v}", + # "text": self.edge_text[edge], + # "color": self.edge_color[edge], + # } + # ) + # return compas_blender.draw_texts(labels, collection=self.edgelabelcollection) + + # def draw_facelabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: + # """Draw labels for a selection of faces. + + # Parameters + # ---------- + # text : dict[int, str], optional + # A dictionary of face labels as face-text pairs. + # The default value is None, in which case every face will be labeled with its identifier. + + # Returns + # ------- + # list[:blender:`bpy.types.Object`] + + # """ + # self.face_text = text + # labels = [] + # for face in self.face_text: + # labels.append( + # { + # "pos": centroid_points([self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)]), + # "name": "{}.facelabel.{}".format(self.volmesh.name, face), + # "text": self.face_text[face], + # "color": self.face_color[face], + # } + # ) + # return compas_blender.draw_texts(labels, collection=self.collection) diff --git a/src/compas_blender/conversions/__init__.py b/src/compas_blender/conversions/__init__.py index 5ff1e7bbf74..c7460d02ee5 100644 --- a/src/compas_blender/conversions/__init__.py +++ b/src/compas_blender/conversions/__init__.py @@ -1,5 +1,44 @@ -from ._geometry import BlenderGeometry -from .curve import BlenderCurve -from .mesh import BlenderMesh +from .colors import color_to_blender_material -__all__ = ["BlenderGeometry", "BlenderCurve", "BlenderMesh"] +from .geometry import pointcloud_to_blender +from .geometry import polygon_to_blender_mesh +from .geometry import sphere_to_blender_mesh +from .geometry import cylinder_to_blender_mesh + +from .transformations import transformation_to_blender +from .transformations import transformation_to_compas + +from .curves import line_to_blender_curve +from .curves import polyline_to_blender_curve +from .curves import circle_to_blender_curve +from .curves import nurbscurve_to_blender_curve + +from .surfaces import nurbssurface_to_blender_surface + +from .meshes import mesh_to_blender +from .meshes import vertices_and_faces_to_blender_mesh +from .meshes import mesh_to_compas +from .meshes import bmesh_to_compas +from .meshes import monkey_to_compas +from .meshes import meshobj_to_compas + +__all__ = [ + "color_to_blender_material", + "pointcloud_to_blender", + "polygon_to_blender_mesh", + "sphere_to_blender_mesh", + "cylinder_to_blender_mesh", + "line_to_blender_curve", + "polyline_to_blender_curve", + "circle_to_blender_curve", + "nurbscurve_to_blender_curve", + "nurbssurface_to_blender_surface", + "mesh_to_blender", + "vertices_and_faces_to_blender_mesh", + "mesh_to_compas", + "bmesh_to_compas", + "monkey_to_compas", + "meshobj_to_compas", + "transformation_to_blender", + "transformation_to_compas", +] diff --git a/src/compas_blender/conversions/_geometry.py b/src/compas_blender/conversions/_geometry.py deleted file mode 100644 index 124bb876e67..00000000000 --- a/src/compas_blender/conversions/_geometry.py +++ /dev/null @@ -1,149 +0,0 @@ -import bpy -from compas.geometry import Point - - -class BlenderGeometry: - """Base class for Blender Geometry and Object wrappers. - - Blender scene objects have an underlying data block that represents the actual geometry of the object. - Multiple objects can share the same data block. - - Every object adds a "location", "rotation", and "scale" to the data block to place it in the scene. - To change an individual object without changing the underlying data block, change its location, rotation or scale. - Change a data block directly to change all the connected objects. - """ - - def __init__(self): - self._object = None - self._geometry = None - - @property - def object(self): - """:blender:`bpy.types.Object` - The Blender scene object.""" - return self._object - - @object.setter - def object(self, obj): - raise NotImplementedError - - @property - def geometry(self): - """The data block.""" - return self._geometry - - @geometry.setter - def geometry(self, data): - raise NotImplementedError - - @property - def type(self): - """:obj:`str` - The type of Blender object.""" - if self.object: - return self.object.type - - @property - def name(self): - """:obj:`str` - The name of the Blender object.""" - if self.object: - return self.object.name - - @name.setter - def name(self, value): - if self.object: - self.object.name = value - - @property - def location(self): - """:class:`~compas.geometry.Point` - The location of the Blender object.""" - return Point(*self.object.location) - - @location.setter - def location(self, location): - self.object.location = list(location) - - @classmethod - def from_object(cls, obj): - """Construct a Blender object wrapper from an existing Blender object. - - Parameters - ---------- - obj : :blender:`bpy.types.Object` - The Blender object. - - Returns - ------- - :class:`~compas_blender.conversions.BlenderGeometry` - The Blender object wrapper. - - Raises - ------ - :class:`ConversionError` - If the geometry of the Blender scene object cannot be converted to the geometry type of the wrapper. - """ - wrapper = cls() - wrapper.object = obj - return wrapper - - @classmethod - def from_geometry(cls, geometry): - """Construct a Blender object wrapper from an existing Blender data block. - - Parameters - ---------- - geometry : Blender data block - The Blender data block. - - Returns - ------- - :class:`~compas_blender.conversions.BlenderGeometry` - The Blender object wrapper. - - Raises - ------ - :class:`ConversionError` - If the geometry of the Blender data block cannot be converted to the geometry type of the wrapper. - """ - wrapper = cls() - wrapper.geometry = geometry - return wrapper - - @classmethod - def from_name(cls, name): - """Construct a Blender object wrapper from an existing Blender object. - - Parameters - ---------- - name : :obj:`str` - The name of the Blender scene object. - - Returns - ------- - :class:`~compas_blender.conversions.BlenderGeometry` - The Blender object wrapper. - - Raises - ------ - :class:`ConversionError` - If the geometry of the Blender data block cannot be converted to the geometry type of the wrapper. - """ - wrapper = cls() - wrapper.object = bpy.data.objects[name] - return wrapper - - def to_compas(self): - raise NotImplementedError - - def transform(self, T): - """Transform the Blender object. - - Parameters - ---------- - T : :class:`~compas.geometry.Transformation` - The transformation matrix. - - Returns - ------- - None - The Blender object is transformed in place. - """ - raise NotImplementedError diff --git a/src/compas_blender/conversions/breps.py b/src/compas_blender/conversions/breps.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/compas_blender/conversions/colors.py b/src/compas_blender/conversions/colors.py new file mode 100644 index 00000000000..3d679c4a220 --- /dev/null +++ b/src/compas_blender/conversions/colors.py @@ -0,0 +1,23 @@ +import bpy # type: ignore + +from compas.colors import Color + + +def color_to_blender_material(color: Color) -> bpy.types.Material: + """Convert a COMPAS color to a Blender material. + + Parameters + ---------- + color : :class:`compas.colors.Color` + A COMPAS color. + + Returns + ------- + :blender:`bpy.types.Material` + A Blender material. + + """ + name = "-".join(["{0:.3f}".format(i) for i in color.rgba]) + material = bpy.data.materials.get(name, bpy.data.materials.new(name)) + material.diffuse_color = color.rgba + return material diff --git a/src/compas_blender/conversions/curve.py b/src/compas_blender/conversions/curve.py deleted file mode 100644 index 308a29cbb6e..00000000000 --- a/src/compas_blender/conversions/curve.py +++ /dev/null @@ -1,53 +0,0 @@ -# from mathutils.geometry import interpolate_bezier - -# from compas.geometry import add_vectors -from ._geometry import BlenderGeometry - - -class BlenderCurve(BlenderGeometry): - """Wrapper for Blender curves. - - Examples - -------- - .. code-block:: python - - pass - """ - - @property - def geometry(self): - """:blender:`bpy.types.Curve` - The curve geometry data block.""" - return self._geometry - - @geometry.setter - def geometry(self, datablock): - self._object = None - self._geometry = datablock - - def to_compas(self): - """Convert the curve to a COMPAS curve. - - Returns - ------- - :class:`~compas_rhino.geometry.RhinoNurbsCurve` - """ - from compas.geometry import NurbsCurve - - curve = NurbsCurve() - curve.rhino_curve = self.geometry - return curve - - # def control_points(self): - # return self.geometry.splines[0].bezier_points - - # def control_point_coordinates(self): - # points = self.control_points() - # middle = [list(i.co) for i in points] - # left = [list(i.handle_left) for i in points] - # right = [list(i.handle_right) for i in points] - # return middle, left, right - - # def divide(self, number_of_segments): - # m, l, r = self.control_point_coordinates() - # points = [list(i) for i in interpolate_bezier(m[0], r[0], l[1], m[1], number_of_segments + 1)] - # return [add_vectors(self.location, point) for point in points] diff --git a/src/compas_blender/conversions/curves.py b/src/compas_blender/conversions/curves.py new file mode 100644 index 00000000000..b39b3d8298b --- /dev/null +++ b/src/compas_blender/conversions/curves.py @@ -0,0 +1,126 @@ +from typing import Optional + +import bpy # type: ignore + +from compas.geometry import Circle +from compas.geometry import Line +from compas.geometry import Polyline +from compas.geometry import NurbsCurve + + +# ============================================================================= +# To Blender +# ============================================================================= + + +def line_to_blender_curve(line: Line) -> bpy.types.Curve: + """Convert a COMPAS line to a Blender curve. + + Parameters + ---------- + line : :class:`compas.geometry.Line` + A COMPAS line. + + Returns + ------- + :class:`bpy.types.Curve` + A Blender curve. + + """ + curve = bpy.data.curves.new(name=line.name, type="CURVE") + curve.dimensions = "3D" + + spline = curve.splines.new("POLY") + spline.points.add(1) + + start = line.start + end = line.end + + spline.points[0].co = [*start, 1.0] + spline.points[1].co = [*end, 1.0] + + spline.order_u = 1 + + return curve + + +def polyline_to_blender_curve(polyline: Polyline, name: Optional[str] = None) -> bpy.types.Curve: + """Convert a COMPAS polyline to a Blender curve. + + Parameters + ---------- + polyline : :class:`compas.geometry.Polyline` + A COMPAS polyline. + + Returns + ------- + :class:`bpy.types.Curve` + A Blender curve. + + """ + curve = bpy.data.curves.new(name=name or polyline.name, type="CURVE") + curve.dimensions = "3D" + + spline = curve.splines.new("POLY") + spline.points.add(len(polyline.points) - 1) + + for i, (point) in enumerate(polyline.points): + spline.points[i].co = [point[0], point[1], point[2], 1.0] + + spline.order_u = 1 + + return curve + + +def circle_to_blender_curve(circle: Circle) -> bpy.types.Curve: + """Convert a COMPAS circle to a Blender circle. + + Parameters + ---------- + circle : :class:`compas.geometry.Circle` + A COMPAS circle. + + Returns + ------- + :class:`bpy.types.Curve` + A Blender curve. + + """ + raise NotImplementedError + + +def nurbscurve_to_blender_curve(nurbscurve: NurbsCurve) -> bpy.types.Curve: + """Convert a COMPAS NURBS curve to a Blender curve. + + Parameters + ---------- + nurbscurve : :class:`compas.geometry.NurbsCurve` + A COMPAS NURBS curve. + + Returns + ------- + :class:`bpy.types.Curve` + A Blender curve. + + """ + curve = bpy.data.curves.new(name=nurbscurve.name, type="CURVE") + curve.dimensions = "3D" + + spline = curve.splines.new("NURBS") + spline.points.add(len(nurbscurve.points) - 1) + + for i, (point, weight) in enumerate(zip(nurbscurve.points, nurbscurve.weights)): + spline.points[i].co = [point[0], point[1], point[2], weight] + spline.points[i].weight = weight + + spline.order_u = nurbscurve.order + spline.use_cyclic_u = nurbscurve.is_periodic + spline.use_endpoint_u = True + spline.use_bezier_u = False + + return curve + + +# ============================================================================= +# To COMPAS +# ============================================================================= diff --git a/src/compas_blender/conversions/geometry.py b/src/compas_blender/conversions/geometry.py new file mode 100644 index 00000000000..25f2ff87c82 --- /dev/null +++ b/src/compas_blender/conversions/geometry.py @@ -0,0 +1,164 @@ +from typing import Optional + +import bpy # type: ignore + +# import bmesh # type: ignore +# import mathutils # type: ignore + +from compas.geometry import Point +from compas.geometry import Pointcloud +from compas.geometry import Sphere +from compas.geometry import Cylinder + + +# ============================================================================= +# To Blender +# ============================================================================= + + +def point_to_blender_sphere(point: Point) -> bpy.types.Object: + """Convert a COMPAS point to a Blender sphere. + + Parameters + ---------- + point : :class:`compas.geometry.Point` + A COMPAS point. + + Returns + ------- + :blender:`bpy.types.Object` + A Blender sphere object. + + """ + raise NotImplementedError + + +def pointcloud_to_blender( + pointcloud: Pointcloud, radius: float = 0.05, u: int = 16, v: int = 16, name: Optional[str] = None +) -> bpy.types.Object: + """Convert a COMPAS pointcloud to a Blender pointcloud. + + Parameters + ---------- + pointcloud : list of :class:`compas.geometry.Point` + A COMPAS pointcloud. + radius : float, optional + The radius of the spheres. + u : int, optional + Number of faces in the "u" direction. + v : int, optional + Number of faces in the "v" direction. + name : str, optional + The name of the Blender object. + + Returns + ------- + :blender:`bpy.types.Mesh` + A Blender pointcloud mesh. + + """ + vertices = [] + faces = [] + for point in pointcloud: + sphere = Sphere.from_point_and_radius(point, radius) + vs, fs = sphere.to_vertices_and_faces(u=u, v=v) + vertices += vs + faces += [[f + len(vertices) for f in face] for face in fs] + mesh = bpy.data.meshes.new(name or pointcloud.name) + mesh.from_pydata(vertices, [], faces) + mesh.validate(verbose=False) + mesh.update(calc_edges=True) + return mesh + + # mesh = bpy.data.meshes.new(name or pointcloud.name) + # bm = bmesh.new() + # for point in pointcloud: + # bmesh.ops.create_uvsphere( + # bm, + # u_segments=u, + # v_segments=v, + # radius=radius, + # matrix=mathutils.Matrix.Translation(point), + # calc_uvs=False, + # ) + # bm.to_mesh(mesh) + # bm.free() + # return mesh + + +def polygon_to_blender_mesh( + points: list[list[float]], + name: Optional[str] = None, +) -> bpy.types.Mesh: + """Convert a list of vertices and faces to a Blender mesh. + + Parameters + ---------- + points : list[point] + A polygon defined as a list of points. + + Returns + ------- + :blender:`bpy.types.Mesh` + A Blender mesh. + + """ + bmesh = bpy.data.meshes.new(name or "Polygon") + bmesh.from_pydata(points, [], [list(range(len(points)))]) + bmesh.update(calc_edges=True) + return bmesh + + +def sphere_to_blender_mesh(sphere: Sphere, u: int = 16, v: int = 16, name: Optional[str] = None) -> bpy.types.Mesh: + """Convert a COMPAS sphere to a Blender mesh. + + Parameters + ---------- + sphere : :class:`compas.geometry.Sphere` + A COMPAS sphere. + + Returns + ------- + :blender:`bpy.types.Mesh` + A Blender mesh. + + """ + vertices, faces = sphere.to_vertices_and_faces(u=u, v=v) + + mesh = bpy.data.meshes.new(name or "Sphere") + mesh.from_pydata(vertices, [], faces) + mesh.update(calc_edges=True) + + return mesh + + +def cylinder_to_blender_mesh(cylinder: Cylinder, u: int = 16, name: Optional[str] = None) -> bpy.types.Mesh: + """Convert a COMPAS cylinder to a Blender mesh. + + Parameters + ---------- + cylinder : :class:`compas.geometry.Cylinder` + A COMPAS cylinder. + u : int, optional + Number of faces in the "u" direction. + name : str, optional + The name of the Blender mesh. + + Returns + ------- + :blender:`bpy.types.Mesh` + A Blender mesh. + + """ + vertices, faces = cylinder.to_vertices_and_faces(u=u) + + mesh = bpy.data.meshes.new(name or "Cylinder") + mesh.from_pydata(vertices, [], faces) + mesh.update(calc_edges=True) + + return mesh + + +# ============================================================================= +# To COMPAS +# ============================================================================= diff --git a/src/compas_blender/conversions/mesh.py b/src/compas_blender/conversions/mesh.py deleted file mode 100644 index 43fae3b9a91..00000000000 --- a/src/compas_blender/conversions/mesh.py +++ /dev/null @@ -1,250 +0,0 @@ -import bpy -import bmesh - -from compas.datastructures import Mesh -from compas.geometry import Point - -from ._geometry import BlenderGeometry - - -class BlenderMesh(BlenderGeometry): - """Wrapper for Blender meshes. - - Attributes - ---------- - object : :blender:`bpy.types.Object` - The Blender scene object. - geometry : :blender:`bpy.types.Mesh` - The mesh data block. - bmesh : :blender:`bpy.types.BMesh` - The mesh data structure. - location : :class:`~compas.geometry.Point` - The location of the object in the scene. - vertices : List[:class:`~compas.geometry.Point`] - The mesh vertex locations. - faces : List[List[:obj:`int`]] - The mesh face vertices. - - Examples - -------- - .. code-block:: python - - import os - import compas - from compas_blender.conversions import BlenderMesh - - mesh = BlenderMesh.from_monkey().to_compas() - mesh = mesh.subdivide(k=2) - - path = os.path.join(os.path.expanduser(~), 'Desktop', 'monkey.json') - - compas.json_dump(mesh, path) - - """ - - @property - def object(self): - return self._object - - @object.setter - def object(self, obj): - mesh = bpy.data.meshes.new_from_object(obj) - self._object = obj - self._geometry = mesh - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, data): - self._object = None - self._geometry = data - - @property - def bmesh(self): - return bmesh.from_edit_mesh(self.mesh) - - @property - def location(self): - if self.object: - return Point(self.object.location) - return Point(0, 0, 0) - - @property - def vertices(self): - point = self.location - return [point + list(vertex.co) for vertex in self.geometry.vertices] - - @property - def faces(self): - return [list(face.vertices) for face in self.geometry.polygons] - - @classmethod - def from_bmesh(cls, bm, name=None, free=True): - """Construct a Blender mesh wrappper from a BMesh. - - Parameters - ---------- - bm : :blender:`bpy.types.BMesh` - The Blender mesh data structure. - name : :obj:`str`, optional - The name of the data block. - free : :obj:`bool`, optional - Free the data structure once the data block is created. - - Returns - ------- - :class:`~compas_blender.conversions.BlenderMesh` - - """ - data = bpy.data.meshes.new(name or "Mesh") - bm.to_mesh(data) - if free: - bm.free() - mesh = cls() - mesh.geometry = data - return mesh - - @classmethod - def from_monkey(cls, name=None): - """Construct a Blender mesh wrappper from the Blender monkey. - - Parameters - ---------- - name : :obj:`str`, optional - The name of the data block. - - Returns - ------- - :class:`~compas_blender.conversions.BlenderMesh` - - """ - bm = bmesh.new() - bmesh.ops.create_monkey(bm) - data = bpy.data.meshes.new(name or "Mesh") - bm.to_mesh(data) - bm.free() - mesh = cls() - mesh.geometry = data - return mesh - - def to_compas(self, cls=None): - """Convert the Blender mesh to a COMPAS mesh. - - Parameters - ---------- - cls : :class:`~compas.datastructures.Mesh`, optional - The type of COMPAS mesh. - - Returns - ------- - :class:`~compas.datastructure.Mesh` - - """ - cls = cls or Mesh - return cls.from_vertices_and_faces(self.vertices, self.faces) - - # def get_vertex_coordinates(self, vertex): - # return add_vectors(self.location, self.geometry.vertices[vertex].co) - - # def get_vertices_coordinates(self): - # xyzs = [vertex.co for vertex in self.geometry.vertices] - # return {vertex: add_vectors(self.location, xyz) for vertex, xyz in enumerate(xyzs)} - - # def set_vertices_coordinates(self, xyzs): - # for vertex, xyz in xyzs.items(): - # self.geometry.vertices[vertex].co = subtract_vectors(xyz, self.location) - - # def get_vertices_colors(self, vertices=None): - # colors = {} - # col = self.geometry.vertex_colors.active - # if col: - # if not vertices: - # vertices = range(len(self.geometry.vertices)) - # for face in self.geometry.polygons: - # for i in face.loop_indices: - # j = self.geometry.loops[i].vertex_index - # if (j in vertices) and (not colors.get(j, None)): - # colors[j] = list(col.data[i].color)[:3] - # return colors - - # def set_vertices_colors(self, colors): - # if self.geometry.vertex_colors: - # col = self.geometry.vertex_colors.active - # else: - # col = self.geometry.vertex_colors.new() - # for face in self.geometry.polygons: - # for i in face.loop_indices: - # j = self.geometry.loops[i].vertex_index - # if j in colors: - # col.data[i].color = list(colors[j]) + [1] - - # def unset_vertices_colors(self): - # vertex_colors = self.geometry.vertex_colors - # while vertex_colors: - # vertex_colors.remove(vertex_colors[0]) - - # def get_edge_vertex_indices(self, edge): - # return list(self.geometry.edges[edge].vertices) - - # def get_edges_vertex_indices(self, edges=None): - # if not edges: - # edges = range(len(self.geometry.edges)) - # return {edge: self.get_edge_vertex_indices(edge=edge) for edge in edges} - - # def edge_length(self, edge): - # u, v = self.geometry.edges[edge].vertices - # sp, ep = [list(self.geometry.vertices[i].co) for i in [u, v]] - # return distance_point_point(sp, ep) - - # def edges_lengths(self, edges=None): - # if not edges: - # edges = range(len(self.geometry.edges)) - # return {edge: self.edge_length(edge=edge) for edge in edges} - - # def get_face_vertex_indices(self, face): - # return list(self.geometry.polygons[face].vertices) - - # def get_faces_vertex_indices(self, faces=None): - # if not faces: - # faces = range(len(self.geometry.polygons)) - # return {face: self.get_face_vertex_indices(face=face) for face in faces} - - # def face_normal(self, face): - # return list(self.geometry.polygons[face].normal) - - # def faces_normals(self, faces=None): - # if not faces: - # faces = range(len(self.geometry.polygons)) - # return {face: self.face_normal(face=face) for face in faces} - - # def face_area(self, face): - # return self.geometry.polygons[face].area - - # def faces_areas(self, faces=None): - # if not faces: - # faces = range(len(self.geometry.polygons)) - # return {face: self.face_area(face=face) for face in faces} - - # def bevel(self, width=0.2, segments=1, only_vertices=False): - # self.object.modifiers.new('bevel', type='BEVEL') - # self.object.modifiers['bevel'].width = width - # self.object.modifiers['bevel'].segments = segments - # self.object.modifiers['bevel'].use_only_vertices = only_vertices - # self.refresh() - - # def subdivide(self, levels=1, type='SIMPLE'): - # self.object.modifiers.new('subdivision', type='SUBSURF') - # self.object.modifiers['subdivision'].levels = levels - # self.object.modifiers['subdivision'].subdivision_type = type # or 'CATMULL_CLARK' - # self.refresh() - - # def triangulate(self): - # self.object.modifiers.new('triangulate', type='TRIANGULATE') - # self.refresh() - - # def get_vertices_and_faces(self): - # vertices = self.get_vertices_coordinates() - # faces = self.get_faces_vertex_indices() - # return vertices, faces diff --git a/src/compas_blender/conversions/meshes.py b/src/compas_blender/conversions/meshes.py new file mode 100644 index 00000000000..8f86a77de00 --- /dev/null +++ b/src/compas_blender/conversions/meshes.py @@ -0,0 +1,142 @@ +from typing import Optional +import bpy # type: ignore +import bmesh # type: ignore + +from compas.geometry import Translation +from compas.datastructures import Mesh + + +# ============================================================================= +# To Blender +# ============================================================================= + + +def mesh_to_blender(mesh: Mesh) -> bpy.types.Mesh: + """Convert a COMPAS mesh to a Blender mesh. + + Parameters + ---------- + mesh : :class:`compas.datastructures.Mesh` + A COMPAS mesh. + + Returns + ------- + :class:`bpy.types.Mesh` + A Blender mesh. + + """ + vertices, faces = mesh.to_vertices_and_faces() + return vertices_and_faces_to_blender_mesh(vertices, faces, name=mesh.name) # type: ignore + + +def vertices_and_faces_to_blender_mesh( + vertices: list[list[float]], + faces: list[list[int]], + name: Optional[str] = None, +) -> bpy.types.Mesh: + """Convert a list of vertices and faces to a Blender mesh. + + Parameters + ---------- + vertices : list + A list of vertex coordinates. + faces : list + A list of faces, defined as lists of indices into the list of vertices. + name : str, optional + The name of the mesh. + + Returns + ------- + :class:`bpy.types.Mesh` + A Blender mesh. + + """ + bmesh = bpy.data.meshes.new(name or "Mesh") + bmesh.from_pydata(vertices, [], faces) + bmesh.update(calc_edges=True) + return bmesh + + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def mesh_to_compas(m: bpy.types.Mesh, name=None) -> Mesh: + """Convert a Blender mesh to a COMPAS mesh. + + Parameters + ---------- + m : :class:`bpy.types.Mesh` + A Blender mesh. + + Returns + ------- + :class:`compas.datastructures.Mesh` + A COMPAS mesh. + + """ + vertices = [vertex.co for vertex in m.vertices] + faces = [face.vertices for face in m.polygons] + mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.name = name + return mesh + + +def bmesh_to_compas(bm: bmesh.types.BMesh, name=None) -> Mesh: + """Convert a Blender BMesh to a COMPAS mesh. + + A BMesh is the data structure used by Blender to represent meshes. + + Parameters + ---------- + bm : :class:`bmesh.types.BMesh` + A Blender BMesh. + + Returns + ------- + :class:`compas.datastructures.Mesh` + A COMPAS mesh. + + """ + data = bpy.data.meshes.new(name or "Mesh") + bm.to_mesh(data) + bm.free() + return mesh_to_compas(data, name=name) + + +def monkey_to_compas(): + """Construct a COMPAS mesh from the Blender monkey. + + Returns + ------- + :class:`compas.datastructures.Mesh` + A COMPAS mesh. + + """ + bm = bmesh.new() + bmesh.ops.create_monkey(bm) + data = bpy.data.meshes.new("Mesh") + bm.to_mesh(data) + bm.free() + return mesh_to_compas(data, name="Suzanne") + + +def meshobj_to_compas(obj: bpy.types.Object) -> Mesh: + """Convert a Blender mesh object to a COMPAS mesh. + + Parameters + ---------- + obj : :class:`bpy.types.Object` + A Blender mesh object. + + Returns + ------- + :class:`compas.datastructures.Mesh` + A COMPAS mesh. + + """ + mesh = mesh_to_compas(obj.data) + T = Translation.from_vector(obj.location) + mesh.transform(T) + return mesh diff --git a/src/compas_blender/conversions/surfaces.py b/src/compas_blender/conversions/surfaces.py new file mode 100644 index 00000000000..ead438fbf3a --- /dev/null +++ b/src/compas_blender/conversions/surfaces.py @@ -0,0 +1,48 @@ +import bpy # type: ignore +from compas.geometry import NurbsSurface + + +# ============================================================================= +# To Blender +# ============================================================================= + + +def nurbssurface_to_blender_surface(nurbssurface: NurbsSurface, u=32, v=32) -> bpy.types.Curve: + """Convert a COMPAS NURBS surface to a Blender surface. + + Parameters + ---------- + nurbssurface : :class:`compas.geometry.NurbsSurface` + A COMPAS NURBS surface. + + Returns + ------- + :class:`bpy.types.Curve` + A Blender surface. + + """ + surf = bpy.data.curves.new(name=nurbssurface.name, type="SURFACE") + surf.dimensions = "3D" + surf.resolution_u = u + surf.resolution_v = v + + # add the U(V) splines + for points, weights in zip(nurbssurface.points, nurbssurface.weights): + spline = surf.splines.new("NURBS") + spline.points.add(len(points) - 1) + + for i, (point, weight) in enumerate(zip(points, weights)): + spline.points[i].co = [point[0], point[1], point[2], weight] + spline.points[i].weight = weight + + spline.use_endpoint_u = True + spline.use_endpoint_v = True + spline.order_u = nurbssurface.degree_u + 1 + spline.order_v = nurbssurface.degree_v + 1 + + return surf + + +# ============================================================================= +# To COMPAS +# ============================================================================= diff --git a/src/compas_blender/conversions/transformations.py b/src/compas_blender/conversions/transformations.py new file mode 100644 index 00000000000..993452414db --- /dev/null +++ b/src/compas_blender/conversions/transformations.py @@ -0,0 +1,46 @@ +import mathutils # type: ignore +from compas.geometry import Transformation + + +# ============================================================================= +# To Blender +# ============================================================================= + + +def transformation_to_blender(transformation): + """Convert a COMPAS transformation to a Blender transformation. + + Parameters + ---------- + transformation : :class:`compas.geometry.Transformation` + A COMPAS transformation. + + Returns + ------- + :class:`mathutils.Matrix` + A Blender transformation. + + """ + return mathutils.Matrix(transformation.matrix) + + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def transformation_to_compas(matrix): + """Convert a Blender transformation to a COMPAS transformation. + + Parameters + ---------- + matrix : :class:`mathutils.Matrix` + A Blender transformation. + + Returns + ------- + :class:`compas.geometry.Transformation` + A COMPAS transformation. + + """ + return Transformation.from_matrix(matrix) diff --git a/src/compas_blender/geometry/curves/__init__.py b/src/compas_blender/geometry/curves/__init__.py new file mode 100644 index 00000000000..609c1fabb14 --- /dev/null +++ b/src/compas_blender/geometry/curves/__init__.py @@ -0,0 +1,33 @@ +from .curve import BlenderCurve +from .nurbs import BlenderNurbsCurve + +from compas.plugins import plugin + + +@plugin(category="factories", requires=["bpy"]) +def new_curve(cls, *args, **kwargs): + curve = object.__new__(BlenderCurve) + curve.__init__(*args, **kwargs) + return curve + + +@plugin(category="factories", requires=["bpy"]) +def new_nurbscurve(cls, *args, **kwargs): + curve = object.__new__(BlenderNurbsCurve) + curve.__init__(*args, **kwargs) + return curve + + +@plugin(category="factories", requires=["bpy"]) +def new_nurbscurve_from_parameters(cls, *args, **kwargs): + return BlenderNurbsCurve.from_parameters(*args, **kwargs) + + +@plugin(category="factories", requires=["bpy"]) +def new_nurbscurve_from_points(cls, *args, **kwargs): + return BlenderNurbsCurve.from_points(*args, **kwargs) + + +# @plugin(category="factories", requires=["bpy"]) +# def new_nurbscurve_from_step(cls, *args, **kwargs): +# return BlenderNurbsCurve.from_step(*args, **kwargs) diff --git a/src/compas_blender/geometry/curves/curve.py b/src/compas_blender/geometry/curves/curve.py new file mode 100644 index 00000000000..31464e61386 --- /dev/null +++ b/src/compas_blender/geometry/curves/curve.py @@ -0,0 +1,259 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.geometry import Curve + +# from compas.geometry import Plane + +# from compas_rhino.conversions import point_to_rhino +# from compas_rhino.conversions import point_to_compas +# from compas_rhino.conversions import vector_to_compas +# from compas_rhino.conversions import xform_to_rhino +# from compas_rhino.conversions import plane_to_compas_frame +# from compas_rhino.conversions import plane_to_rhino +# from compas_rhino.conversions import box_to_compas + + +class BlenderCurve(Curve): + """Class representing a general curve object. + + Parameters + ---------- + name : str, optional + Name of the curve. + + Attributes + ---------- + dimension : int, read-only + The spatial dimension of the curve. + domain : tuple[float, float], read-only + The parameter domain. + start : :class:`~compas.geometry.Point`, read-only + The point corresponding to the start of the parameter domain. + end : :class:`~compas.geometry.Point`, read-only + The point corresponding to the end of the parameter domain. + is_closed : bool, read-only + True if the curve is closed. + is_periodic : bool, read-only + True if the curve is periodic. + + Other Attributes + ---------------- + native_curve : :rhino:`Curve` + The underlying Rhino curve. + + """ + + def __init__(self, name=None): + super(BlenderCurve, self).__init__(name=name) + self._native_curve = None + + # def __eq__(self, other): + # return self.native_curve.IsEqual(other.native_curve) # type: ignore + + # ============================================================================== + # Data + # ============================================================================== + + # ============================================================================== + # Properties + # ============================================================================== + + @property + def native_curve(self): + return self._native_curve + + @native_curve.setter + def native_curve(self, curve): + self._native_curve = curve + + @property + def splines(self): + if self.native_curve: + return self.native_curve.splines + + @property + def dimension(self): + if self.native_curve: + return self.native_curve.splines[0].dimensions + + @property + def domain(self): + if self.native_curve: + return 0, 1 + + @property + def start(self): + if self.native_curve: + return self.native_curve.splines[0].points[0].co + + @property + def end(self): + if self.native_curve: + return self.native_curve.splines[0].points[-1].co + + @property + def is_closed(self): + if self.native_curve: + pass + + @property + def is_periodic(self): + if self.native_curve: + return self.native_curve.splines[0].use_cyclic_u + + # ============================================================================== + # Constructors + # ============================================================================== + + @classmethod + def from_native(cls, native_curve): + """Construct a curve from an existing Rhino curve. + + Parameters + ---------- + native_curve : bpy.types.Curve + + Returns + ------- + :class:`~compas_rhino.geometry.BlenderCurve` + + """ + curve = cls() + curve.native_curve = native_curve + return curve + + # ============================================================================== + # Conversions + # ============================================================================== + + # ============================================================================== + # Methods + # ============================================================================== + + # def copy(self): + # """Make an independent copy of the current curve. + + # Returns + # ------- + # :class:`~compas_rhino.geometry.RhinoCurve` + + # """ + # cls = type(self) + # curve = cls() + # curve.native_curve = self.native_curve.Duplicate() # type: ignore + # return curve + + # def transform(self, T): + # """Transform this curve. + + # Parameters + # ---------- + # T : :class:`~compas.geometry.Transformation` + # A COMPAS transformation. + + # Returns + # ------- + # None + + # """ + # self.native_curve.Transform(xform_to_rhino(T)) # type: ignore + + # def reverse(self): + # """Reverse the parametrisation of the curve. + + # Returns + # ------- + # None + + # """ + # self.native_curve.Reverse() # type: ignore + + # def point_at(self, t): + # """Compute a point on the curve. + + # Parameters + # ---------- + # t : float + # The value of the curve parameter. Must be between 0 and 1. + + # Returns + # ------- + # :class:`~compas.geometry.Point` + # the corresponding point on the curve. + + # """ + # point = self.native_curve.PointAt(t) # type: ignore + # return point_to_compas(point) + + # def tangent_at(self, t): + # """Compute the tangent vector at a point on the curve. + + # Parameters + # ---------- + # t : float + # The value of the curve parameter. Must be between 0 and 1. + + # Returns + # ------- + # :class:`~compas.geometry.Vector` + # The corresponding tangent vector. + + # """ + # vector = self.native_curve.TangentAt(t) # type: ignore + # return vector_to_compas(vector) + + # def curvature_at(self, t): + # """Compute the curvature at a point on the curve. + + # Parameters + # ---------- + # t : float + # The value of the curve parameter. Must be between 0 and 1. + + # Returns + # ------- + # :class:`~compas.geometry.Vector` + # The corresponding curvature vector. + + # """ + # vector = self.native_curve.CurvatureAt(t) # type: ignore + # return vector_to_compas(vector) + + # def frame_at(self, t): + # """Compute the local frame at a point on the curve. + + # Parameters + # ---------- + # t : float + # The value of the curve parameter. + + # Returns + # ------- + # :class:`~compas.geometry.Frame` + # The corresponding local frame. + + # """ + # t, plane = self.native_curve.FrameAt(t) # type: ignore + # return plane_to_compas_frame(plane) + + # def torsion_at(self, t): + # """Compute the torsion of the curve at a parameter. + + # Parameters + # ---------- + # t : float + # The value of the curve parameter. + + # Returns + # ------- + # float + # The torsion value. + + # """ + # return self.native_curve.TorsionAt(t) # type: ignore + + # ============================================================================== + # Methods continued + # ============================================================================== diff --git a/src/compas_blender/geometry/curves/nurbs.py b/src/compas_blender/geometry/curves/nurbs.py new file mode 100644 index 00000000000..78bc4bf6d15 --- /dev/null +++ b/src/compas_blender/geometry/curves/nurbs.py @@ -0,0 +1,240 @@ +import bpy # type: ignore + +# from itertools import groupby +# from compas.geometry import Point +from compas.geometry import NurbsCurve + +# from compas_blender.conversions import point_to_blender +# from compas_blender.conversions import point_to_compas +# from compas_blender.conversions import line_to_blender + +from compas.geometry import Point + +from .curve import BlenderCurve + + +def native_curve_from_parameters(points, weights, knots, multiplicities, degree, is_periodic, name="NurbsCurve"): + """Create a Blender NurbsCurve from explicit curve parameters. + + Parameters + ---------- + points : list[:class:`~compas.geometry.Point`] + The control points. + weights : list[float] + The control point weights. + knots : list[float] + The curve knots, without duplicates. + multiplicities : list[int] + The multiplicities of the knots. + degree : int + The degree of the curve. + name : str, optional + Name of the curve. + + Returns + ------- + :class:`bpy.types.Curve` + A Blender NurbsCurve. + + Notes + ----- + This will construct a Blender curve with one spline segment and the spline segment is defined as a NURBS curve. + In Blender, you cannot edit the knot vectors directly, but you can influence them through the Endpoint and Bézier options. + Note that, the Endpoint and Bézier settings only apply to open NURBS curves. + + """ + curve = bpy.data.curves.new(name=name, type="CURVE") + curve.dimensions = "3D" + + spline = curve.splines.new("NURBS") + spline.points.add(len(points) - 1) + + for i, (point, weight) in enumerate(zip(points, weights)): + spline.points[i].co = [point[0], point[1], point[2], weight] + spline.points[i].weight = weight + + spline.order_u = degree + 1 + spline.use_cyclic_u = is_periodic + spline.use_endpoint_u = True + spline.use_bezier_u = False + + return curve + + +class BlenderNurbsCurve(NurbsCurve, BlenderCurve): + """Class representing a NURBS curve based on the NurbsCurve of Blender.Geometry. + + Parameters + ---------- + name : str, optional + Name of the curve. + + Attributes + ---------- + points : list[:class:`~compas.geometry.Point`], read-only + The control points of the curve. + weights : list[float], read-only + The weights of the control points. + knots : list[float], read-only + The knot vector, without duplicates. + multiplicities : list[int], read-only + The multiplicities of the knots in the knot vector. + knotsequence : list[float], read-only + The knot vector, with repeating values according to the multiplicities. + continuity : int, read-only + The degree of continuity of the curve. + degree : int, read-only + The degree of the curve. + order : int, read-only + The order of the curve (degree + 1). + is_rational : bool, read-only + True is the curve is rational. + + """ + + def __init__(self, name=None): + super(BlenderNurbsCurve, self).__init__(name=name) + self.native_curve = None + + # ============================================================================== + # Data + # ============================================================================== + + # @property + # def data(self): + # # add superfluous knots + # # for compatibility with all/most other NURBS implementations + # # https://developer.blender3d.com/guides/opennurbs/superfluous-knots/ + # multiplicities = self.multiplicities[:] + # multiplicities[0] += 1 + # multiplicities[-1] += 1 + # return { + # "points": [point.data for point in self.points], + # "weights": self.weights, + # "knots": self.knots, + # "multiplicities": multiplicities, + # "degree": self.degree, + # "is_periodic": self.is_periodic, + # } + + # @data.setter + # def data(self, data): + # points = [Point.from_data(point) for point in data["points"]] + # weights = data["weights"] + # knots = data["knots"] + # multiplicities = data["multiplicities"] + # degree = data["degree"] + # # is_periodic = data['is_periodic'] + # # have not found a way to actually set this + # # not sure if that is actually possible... + # self.native_curve = native_curve_from_parameters(points, weights, knots, multiplicities, degree) + + # ============================================================================== + # Blender Properties + # ============================================================================== + + # ============================================================================== + # Properties + # ============================================================================== + + @property + def points(self): + if self.native_curve: + return [Point(*point.co) for point in self.native_curve.splines[0].points] + + @property + def weights(self): + if self.native_curve: + return [point.weight for point in self.native_curve.splines[0].points] + + # @property + # def knots(self): + # pass + + # @property + # def knotsequence(self): + # pass + + # @property + # def multiplicities(self): + # pass + + @property + def degree(self): + if self.native_curve: + return self.native_curve.order_u - 1 + + @property + def order(self): + if self.native_curve: + return self.native_curve.order_u + + # @property + # def is_rational(self): + # pass + + # ============================================================================== + # Constructors + # ============================================================================== + + @classmethod + def from_parameters(cls, points, weights, knots, multiplicities, degree, is_periodic=False): + """Construct a NURBS curve from explicit curve parameters. + + Parameters + ---------- + points : list[:class:`~compas.geometry.Point`] + The control points. + weights : list[float] + The control point weights. + knots : list[float] + The curve knots, without duplicates. + multiplicities : list[int] + The multiplicities of the knots. + degree : int + The degree of the curve. + is_periodic : bool, optional + Flag indicating whether the curve is periodic or not. + Note that this parameters is currently not supported. + + Returns + ------- + :class:`~compas_blender.geometry.BlenderNurbsCurve` + + """ + curve = cls() + curve.native_curve = native_curve_from_parameters(points, weights, None, None, degree, is_periodic) + return curve + + @classmethod + def from_points(cls, points, degree=3, is_periodic=False): + """Construct a NURBS curve from control points. + + Parameters + ---------- + points : list[:class:`~compas.geometry.Point`] + The control points. + degree : int, optional + The degree of the curve. + is_periodic : bool, optional + Flag indicating whether the curve is periodic or not. + + Returns + ------- + :class:`~compas_blender.geometry.BlenderNurbsCurve` + + """ + weights = [1.0 for point in points] + knots = None + multiplicities = None + curve = cls() + curve.native_curve = native_curve_from_parameters(points, weights, knots, multiplicities, degree, is_periodic) + return curve + + # ============================================================================== + # Conversions + # ============================================================================== + + # ============================================================================== + # Methods + # ============================================================================== diff --git a/src/compas_blender/ui/__init__.py b/src/compas_blender/ui/__init__.py deleted file mode 100644 index 353b6dbb8c0..00000000000 --- a/src/compas_blender/ui/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -******************************************************************************** -ui -******************************************************************************** - -.. currentmodule:: compas_blender.ui - -Classes -======= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - -""" -from .mouse import Mouse - - -__all__ = ["Mouse"] diff --git a/src/compas_blender/ui/mouse.py b/src/compas_blender/ui/mouse.py deleted file mode 100644 index e8c0d6e00a2..00000000000 --- a/src/compas_blender/ui/mouse.py +++ /dev/null @@ -1,23 +0,0 @@ -import bpy - - -__all__ = [ - "Mouse", -] - - -class Mouse(object): - def __init__(self): - pass - - def OnMouseMove(self, e): - raise NotImplementedError - - def OnMouseDown(self, e): - raise NotImplementedError - - def OnMouseUp(self, e): - raise NotImplementedError - - def xyz(self): - return list(bpy.context.scene.cursor_location.copy()) diff --git a/src/compas_blender/utilities/collections.py b/src/compas_blender/utilities/collections.py index 4c2725e7861..e21220e9500 100644 --- a/src/compas_blender/utilities/collections.py +++ b/src/compas_blender/utilities/collections.py @@ -1,4 +1,4 @@ -import bpy +import bpy # type: ignore from typing import List, Text from compas_blender.utilities import delete_objects diff --git a/src/compas_blender/utilities/data.py b/src/compas_blender/utilities/data.py index d03750cac21..628b3487c29 100644 --- a/src/compas_blender/utilities/data.py +++ b/src/compas_blender/utilities/data.py @@ -1,4 +1,4 @@ -import bpy +import bpy # type: ignore def delete_unused_data(): @@ -28,15 +28,3 @@ def delete_unused_data(): for block in bpy.data.images: if block.users == 0: bpy.data.images.remove(block) - - # for collection in bpy.context.scene.collection.children: - # bpy.context.scene.collection.children.unlink(collection) - - # for block in bpy.data.collections: - # objects = [o for o in block.objects if o.users] - # while objects: - # bpy.data.objects.remove(objects.pop()) - # for collection in block.children: - # block.children.unlink(collection) - # if block.users == 0: - # bpy.data.collections.remove(block) diff --git a/src/compas_blender/utilities/drawing.py b/src/compas_blender/utilities/drawing.py index 27872124ab7..1a51554712f 100644 --- a/src/compas_blender/utilities/drawing.py +++ b/src/compas_blender/utilities/drawing.py @@ -1,11 +1,11 @@ -import bpy - from typing import Dict from typing import List from typing import Union from typing import Tuple from typing import Text +import bpy # type: ignore + from compas_blender.utilities import create_collection from compas.geometry import centroid_points @@ -757,7 +757,7 @@ def draw_surfaces(surfaces: List[Dict], collection: Union[Text, bpy.types.Collec _link_objects(objects, collection) for obj in objects: # select the control points - for s in obj.data.splines: + for s in obj.data.splines: # type: ignore for p in s.points: p.select = True # switch to edit mode diff --git a/src/compas_blender/utilities/objects.py b/src/compas_blender/utilities/objects.py index 05c40e6939c..fc3a2165d93 100644 --- a/src/compas_blender/utilities/objects.py +++ b/src/compas_blender/utilities/objects.py @@ -1,4 +1,4 @@ -import bpy +import bpy # type: ignore from typing import List, Iterable, Text from compas_blender.utilities.data import delete_unused_data diff --git a/src/compas_ghpython/artists/__init__.py b/src/compas_ghpython/artists/__init__.py index 858178c1ca0..9f175a8f6fb 100644 --- a/src/compas_ghpython/artists/__init__.py +++ b/src/compas_ghpython/artists/__init__.py @@ -74,7 +74,6 @@ from compas.plugins import plugin from compas.artists import Artist -from compas.artists import ShapeArtist from compas.geometry import Box from compas.geometry import Capsule @@ -124,23 +123,6 @@ from .brepartist import BrepArtist -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", requires=["Rhino"]) def register_artists(): Artist.register(Box, BoxArtist, context="Grasshopper") @@ -169,7 +151,6 @@ def register_artists(): __all__ = [ "GHArtist", - "ShapeArtist", "BoxArtist", "CapsuleArtist", "CircleArtist", diff --git a/src/compas_ghpython/artists/artist.py b/src/compas_ghpython/artists/artist.py index 51ff01ac92a..306e5f1101d 100644 --- a/src/compas_ghpython/artists/artist.py +++ b/src/compas_ghpython/artists/artist.py @@ -12,4 +12,4 @@ def __init__(self, **kwargs): super(GHArtist, self).__init__(**kwargs) def clear(self): - pass + raise NotImplementedError diff --git a/src/compas_ghpython/artists/boxartist.py b/src/compas_ghpython/artists/boxartist.py index e952e0d8c44..ec186106d11 100644 --- a/src/compas_ghpython/artists/boxartist.py +++ b/src/compas_ghpython/artists/boxartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import ShapeArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class BoxArtist(GHArtist, ShapeArtist): +class BoxArtist(GHArtist, GeometryArtist): """Artist for drawing box shapes. Parameters @@ -17,29 +17,24 @@ class BoxArtist(GHArtist, ShapeArtist): A COMPAS box. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.ShapeArtist` for more info. """ def __init__(self, box, **kwargs): - super(BoxArtist, self).__init__(shape=box, **kwargs) + super(BoxArtist, self).__init__(geometry=box, **kwargs) - def draw(self, color=None): + def draw(self): """Draw the box associated with the artist. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the box. - Default is :attr:`compas.artists.ShapeArtist.color`. - Returns ------- - :rhino:`Rhino.Geometry.Mesh` + :rhino:`Rhino.Geometry.Box` """ - color = Color.coerce(color) or self.color - vertices = [list(vertex) for vertex in self.shape.vertices] - faces = self.shape.faces - mesh = compas_ghpython.draw_mesh(vertices, faces, color=color.rgb255) - return mesh + box = conversions.box_to_rhino(self.geometry) + + if self.transformation: + transformation = conversions.transformation_to_rhino(self.transformation) + box.Transform(transformation) + + return box diff --git a/src/compas_ghpython/artists/brepartist.py b/src/compas_ghpython/artists/brepartist.py index b8e39f3f491..75ed854f123 100644 --- a/src/compas_ghpython/artists/brepartist.py +++ b/src/compas_ghpython/artists/brepartist.py @@ -1,20 +1,27 @@ -from compas_ghpython.artists import GHArtist -from compas_ghpython.utilities import draw_brep +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from compas_rhino import conversions -class BrepArtist(GHArtist): +from compas.artists import GeometryArtist +from .artist import GHArtist + + +class BrepArtist(GHArtist, GeometryArtist): """An artist for drawing a brep in Grasshopper. Parameters - ========== + ---------- brep : :class:`~compas_rhino.geometry.RhinoBrep` The brep to draw. + **kwargs : dict, optional + Additional keyword arguments. """ def __init__(self, brep, **kwargs): - super(BrepArtist, self).__init__(**kwargs) - self._brep = brep + super(BrepArtist, self).__init__(geometry=brep, **kwargs) def draw(self): """Draw the brep as a Grasshopper geometry. @@ -25,4 +32,10 @@ def draw(self): The Grasshopper geometry instance. """ - return draw_brep(self._brep) + brep = conversions.brep_to_rhino(self.geometry) + + if self.transformation: + transformation = conversions.transformation_to_rhino(self.transformation) + brep.Transform(transformation) + + return brep diff --git a/src/compas_ghpython/artists/capsuleartist.py b/src/compas_ghpython/artists/capsuleartist.py index b8284c8d365..371b111fd6b 100644 --- a/src/compas_ghpython/artists/capsuleartist.py +++ b/src/compas_ghpython/artists/capsuleartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import ShapeArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class CapsuleArtist(GHArtist, ShapeArtist): +class CapsuleArtist(GHArtist, GeometryArtist): """Artist for drawing capsule shapes. Parameters @@ -17,37 +17,25 @@ class CapsuleArtist(GHArtist, ShapeArtist): A COMPAS capsule. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.ShapeArtist` for more info. """ def __init__(self, capsule, **kwargs): - super(CapsuleArtist, self).__init__(shape=capsule, **kwargs) + super(CapsuleArtist, self).__init__(geometry=capsule, **kwargs) - def draw(self, color=None, u=None, v=None): + def draw(self): """Draw the capsule associated with the artist. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the capsule. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`CapsuleArtist.u`. - v : int, optional - Number of faces in the "v" direction. - Default is :attr:`CapsuleArtist.v`. - Returns ------- - :rhino:`Rhino.Geometry.Mesh` + list[:rhino:`Rhino.Geometry.Brep`] """ - color = Color.coerce(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] - mesh = compas_ghpython.draw_mesh(vertices, faces, color=color.rgb255) - return mesh + breps = conversions.capsule_to_rhino_brep(self.geometry) + + if self.transformation: + transformation = conversions.transformation_to_rhino(self.transformation) + for geometry in breps: + geometry.Transform(transformation) + + return breps diff --git a/src/compas_ghpython/artists/circleartist.py b/src/compas_ghpython/artists/circleartist.py index 4cfce15af74..32d14eaa9cb 100644 --- a/src/compas_ghpython/artists/circleartist.py +++ b/src/compas_ghpython/artists/circleartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import PrimitiveArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class CircleArtist(GHArtist, PrimitiveArtist): +class CircleArtist(GHArtist, GeometryArtist): """Artist for drawing circles. Parameters @@ -17,37 +17,24 @@ class CircleArtist(GHArtist, PrimitiveArtist): A COMPAS circle. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. """ def __init__(self, circle, **kwargs): - super(CircleArtist, self).__init__(primitive=circle, **kwargs) + super(CircleArtist, self).__init__(geometry=circle, **kwargs) - def draw(self, color=None): + def draw(self): """Draw the circle. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The color of the circle. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - Returns ------- :rhino:`Rhino.Geometry.Circle` """ - color = Color.coerce(color) or self.color - point = list(self.primitive.plane.point) - normal = list(self.primitive.plane.normal) - radius = self.primitive.radius - circles = [ - { - "plane": [point, normal], - "radius": radius, - "color": color.rgb255, - "name": self.primitive.name, - } - ] - return compas_ghpython.draw_circles(circles)[0] + circle = conversions.circle_to_rhino(self.geometry) + + if self.transformation: + transformation = conversions.transformation_to_rhino(self.transformation) + circle.Transform(transformation) + + return circle diff --git a/src/compas_ghpython/artists/coneartist.py b/src/compas_ghpython/artists/coneartist.py index 459a7cc204a..f1d624b9c65 100644 --- a/src/compas_ghpython/artists/coneartist.py +++ b/src/compas_ghpython/artists/coneartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import ShapeArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class ConeArtist(GHArtist, ShapeArtist): +class ConeArtist(GHArtist, GeometryArtist): """Artist for drawing cone shapes. Parameters @@ -17,33 +17,25 @@ class ConeArtist(GHArtist, ShapeArtist): A COMPAS cone. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.ShapeArtist` for more info. """ def __init__(self, cone, **kwargs): - super(ConeArtist, self).__init__(shape=cone, **kwargs) + super(ConeArtist, self).__init__(geometry=cone, **kwargs) - def draw(self, color=None, u=None): + def draw(self): """Draw the cone associated with the artist. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the cone. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`ConeArtist.u` - Returns ------- - :rhino:`Rhino.Geometry.Mesh` + list[:rhino:`Rhino.Geometry.Brep`] """ - color = Color.coerce(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] - mesh = compas_ghpython.draw_mesh(vertices, faces, color=color.rgb255) - return mesh + breps = conversions.cone_to_rhino_brep(self.geometry) + + if self.transformation: + transformation = conversions.transformation_to_rhino(self.transformation) + for geometry in breps: + geometry.Transform(transformation) + + return breps diff --git a/src/compas_ghpython/artists/curveartist.py b/src/compas_ghpython/artists/curveartist.py index f27410839db..96530948bf4 100644 --- a/src/compas_ghpython/artists/curveartist.py +++ b/src/compas_ghpython/artists/curveartist.py @@ -2,11 +2,13 @@ from __future__ import absolute_import from __future__ import division -from compas.artists import CurveArtist +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class CurveArtist(GHArtist, CurveArtist): +class CurveArtist(GHArtist, GeometryArtist): """Artist for drawing curves. Parameters @@ -18,25 +20,24 @@ class CurveArtist(GHArtist, CurveArtist): ---------------- **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`GHArtist` and :class:`~compas.artists.CurveArtist`. """ def __init__(self, curve, **kwargs): - super(CurveArtist, self).__init__(curve=curve, **kwargs) + super(CurveArtist, self).__init__(geometry=curve, **kwargs) - def draw(self, color=None): + def draw(self): """Draw the curve. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the curve. - Default is :attr:`compas.artists.CurveArtist.color`. - Returns ------- :rhino:`Rhino.Geometry.Curve` """ - return self.curve.rhino_curve + geometry = conversions.curve_to_rhino(self.geometry) + + if self.transformation: + transformation = conversions.transformation_to_rhino(self.transformation) + geometry.Transform(transformation) + + return geometry diff --git a/src/compas_ghpython/artists/cylinderartist.py b/src/compas_ghpython/artists/cylinderartist.py index 4004371122e..bbec9df0e8e 100644 --- a/src/compas_ghpython/artists/cylinderartist.py +++ b/src/compas_ghpython/artists/cylinderartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import ShapeArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class CylinderArtist(GHArtist, ShapeArtist): +class CylinderArtist(GHArtist, GeometryArtist): """Artist for drawing cylinder shapes. Parameters @@ -17,33 +17,30 @@ class CylinderArtist(GHArtist, ShapeArtist): A COMPAS cylinder. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.ShapeArtist` for more info. """ def __init__(self, cylinder, **kwargs): - super(CylinderArtist, self).__init__(shape=cylinder, **kwargs) + super(CylinderArtist, self).__init__(geometry=cylinder, **kwargs) - def draw(self, color=None, u=None): + def draw(self, color=None, u=16): """Draw the cylinder associated with the artist. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the box. - Default is :attr:`compas.artists.ShapeArtist.color`. u : int, optional Number of faces in the "u" direction. - Default is :attr:`CylinderArtist.u` Returns ------- :rhino:`Rhino.Geometry.Mesh` """ - color = Color.coerce(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] - mesh = compas_ghpython.draw_mesh(vertices, faces, color=color.rgb255) - return mesh + geometry = conversions.cylinder_to_rhino_brep(self.geometry) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/frameartist.py b/src/compas_ghpython/artists/frameartist.py index ae451964672..0dd27c5cc88 100644 --- a/src/compas_ghpython/artists/frameartist.py +++ b/src/compas_ghpython/artists/frameartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import PrimitiveArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class FrameArtist(GHArtist, PrimitiveArtist): +class FrameArtist(GHArtist, GeometryArtist): """Artist for drawing frames. Parameters @@ -19,7 +19,6 @@ class FrameArtist(GHArtist, PrimitiveArtist): The scale of the vectors representing the axes of the frame. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. Attributes ---------- @@ -37,64 +36,28 @@ class FrameArtist(GHArtist, PrimitiveArtist): """ def __init__(self, frame, scale=1.0, **kwargs): - super(FrameArtist, self).__init__(primitive=frame, **kwargs) + super(FrameArtist, self).__init__(geometry=frame, **kwargs) self.scale = scale - self.color_origin = Color.black() - self.color_xaxis = Color.red() - self.color_yaxis = Color.green() - self.color_zaxis = Color.blue() def draw(self): """Draw the frame. Returns ------- - :rhino:`Rhino.Geometry.Plane` - - """ - return compas_ghpython.draw_frame(self.primitive) - - def draw_origin(self): - """Draw the frame's origin. - - Returns - ------- - :rhino:`Rhino.Geometry.Point` + list[:rhino:`Rhino.Geometry.Point3d`, :rhino:`Rhino.Geometry.Line`] """ - point = {"pos": list(self.primitive.point), "color": self.color_origin.rgb255} - return compas_ghpython.draw_points([point])[0] + geometry = [] - def draw_axes(self): - """Draw the frame's axes. + origin = self.geometry.point + x = self.geometry.point + self.geometry.xaxis * self.scale + y = self.geometry.point + self.geometry.yaxis * self.scale + z = self.geometry.point + self.geometry.zaxis * self.scale - Returns - ------- - list[:rhino:`Rhino.Geometry.Line`] + # geometry.append(conversions.frame_to_rhino(self.geometry)) + geometry.append(conversions.point_to_rhino(self.geometry.point)) + geometry.append(conversions.line_to_rhino([origin, x])) + geometry.append(conversions.line_to_rhino([origin, y])) + geometry.append(conversions.line_to_rhino([origin, z])) - """ - origin = list(self.primitive.point) - x = list(self.primitive.point + self.primitive.xaxis.scaled(self.scale)) - y = list(self.primitive.point + self.primitive.yaxis.scaled(self.scale)) - z = list(self.primitive.point + self.primitive.zaxis.scaled(self.scale)) - lines = [ - { - "start": origin, - "end": x, - "color": self.color_xaxis.rgb255, - "arrow": "end", - }, - { - "start": origin, - "end": y, - "color": self.color_yaxis.rgb255, - "arrow": "end", - }, - { - "start": origin, - "end": z, - "color": self.color_zaxis.rgb255, - "arrow": "end", - }, - ] - return compas_ghpython.draw_lines(lines) + return geometry diff --git a/src/compas_ghpython/artists/lineartist.py b/src/compas_ghpython/artists/lineartist.py index 15ac7290655..1836b90870c 100644 --- a/src/compas_ghpython/artists/lineartist.py +++ b/src/compas_ghpython/artists/lineartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import PrimitiveArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class LineArtist(GHArtist, PrimitiveArtist): +class LineArtist(GHArtist, GeometryArtist): """Artist for drawing lines. Parameters @@ -17,29 +17,23 @@ class LineArtist(GHArtist, PrimitiveArtist): A COMPAS line. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. """ def __init__(self, line, **kwargs): - super(LineArtist, self).__init__(primitive=line, **kwargs) + super(LineArtist, self).__init__(geometry=line, **kwargs) - def draw(self, color=None): + def draw(self): """Draw the line. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the line. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - Returns ------- :rhino:`Rhino.Geometry.Line` """ - color = Color.coerce(color) or self.color - start = list(self.primitive.start) - end = list(self.primitive.end) - lines = [{"start": start, "end": end, "color": color.rgb255}] - return compas_ghpython.draw_lines(lines)[0] + geometry = conversions.line_to_rhino(self.geometry) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/meshartist.py b/src/compas_ghpython/artists/meshartist.py index 8e65dcd46c6..e4d3b6512ba 100644 --- a/src/compas_ghpython/artists/meshartist.py +++ b/src/compas_ghpython/artists/meshartist.py @@ -2,13 +2,14 @@ from __future__ import division from __future__ import print_function -import Rhino -import compas_ghpython -from compas.artists import MeshArtist +from compas.artists import MeshArtist as BaseArtist +from compas.colors import Color +from compas_rhino import conversions +from compas_rhino.artists._helpers import ngon from .artist import GHArtist -class MeshArtist(GHArtist, MeshArtist): +class MeshArtist(GHArtist, BaseArtist): """Artist for drawing mesh data structures. Parameters @@ -17,55 +18,53 @@ class MeshArtist(GHArtist, MeshArtist): A COMPAS mesh. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.MeshArtist` for more info. """ def __init__(self, mesh, **kwargs): super(MeshArtist, self).__init__(mesh=mesh, **kwargs) - def draw(self, color=None): + def draw(self, color=None, vertexcolors=None, facecolors=None, disjoint=False): """Draw the mesh. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The color of the mesh. - Default is the value of :attr:`MeshArtist.default_color`. Returns ------- :rhino:`Rhino.Geometry.Mesh` """ - self.color = color - vertices, faces = self.mesh.to_vertices_and_faces() - return compas_ghpython.draw_mesh(vertices, faces, self.color.rgb255) - - def draw_mesh(self, color=None): - """Draw the mesh as a RhinoMesh. - - This method is an alias for :attr:`MeshArtist.draw`. + # the rhino artist can set an overal color and component colors simultaneously + # because it can set an overall color on the mesh object attributes + # this is not possible in GH (since there is no such object) + # either we set an overall color or we set component colors + if not vertexcolors and not facecolors: + color = Color.coerce(color) or self.color + + vertex_index = self.mesh.vertex_index() # type: ignore + vertex_xyz = self.vertex_xyz - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The color of the mesh. - Default is the value of :attr:`MeshArtist.default_color`. + vertices = [vertex_xyz[vertex] for vertex in self.mesh.vertices()] # type: ignore + faces = [[vertex_index[vertex] for vertex in self.mesh.face_vertices(face)] for face in self.mesh.faces()] # type: ignore - Returns - ------- - :rhino:`Rhino.Geometry.Mesh` + geometry = conversions.vertices_and_faces_to_rhino( + vertices, + faces, + color=color, + vertexcolors=vertexcolors, + facecolors=facecolors, + disjoint=disjoint, + ) - 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. + # if self.transformation: + # geometry.Transform(conversions.transformation_to_rhino(self.transformation)) - """ - return self.draw(color=color) + return geometry - def draw_vertices(self, vertices=None, color=None): + def draw_vertices(self, vertices=None): """Draw a selection of vertices. Parameters @@ -73,69 +72,20 @@ def draw_vertices(self, vertices=None, color=None): vertices : list[int], optional A selection of vertices to draw. Default is None, in which case all vertices are drawn. - color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional - The color specification for the vertices. - The default is the value of :attr:`MeshArtist.default_vertexcolor`. Returns ------- list[:rhino:`Rhino.Geometry.Point3d`] """ - self.vertex_color = color - vertices = vertices or list(self.mesh.vertices()) - vertex_xyz = self.vertex_xyz points = [] - for vertex in vertices: - points.append( - { - "pos": vertex_xyz[vertex], - "name": "{}.vertex.{}".format(self.mesh.name, vertex), - "color": self.vertex_color[vertex].rgb255, - } - ) - return compas_ghpython.draw_points(points) - - def draw_faces(self, faces=None, color=None, join_faces=False): - """Draw a selection of faces. - Parameters - ---------- - faces : list[int], optional - A selection of faces to draw. - The default is None, in which case all faces are drawn. - color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional - The color specification for the faces. - The default color is the value of :attr:`MeshArtist.default_facecolor`. - join_faces : bool, optional - If True, join the individual faces into one mesh. + for vertex in vertices or self.mesh.vertices(): # type: ignore + points.append(conversions.point_to_rhino(self.vertex_xyz[vertex])) - Returns - ------- - list[:rhino:`Rhino.Geometry.Mesh`] + return points - """ - self.face_color = color - faces = faces or list(self.mesh.faces()) - vertex_xyz = self.vertex_xyz - 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": self.face_color[face].rgb255, - } - ) - 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): + def draw_edges(self, edges=None): """Draw a selection of edges. Parameters @@ -143,57 +93,46 @@ def draw_edges(self, edges=None, color=None): edges : list[tuple[int, int]], optional A selection of edges to draw. The default is None, in which case all edges are drawn. - color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional - The color specification for the edges. - The default color is the value of :attr:`MeshArtist.default_edgecolor`. Returns ------- list[:rhino:`Rhino.Geometry.Line`] """ - self.edge_color = color - edges = edges or list(self.mesh.edges()) - vertex_xyz = self.vertex_xyz lines = [] - for edge in edges: - u, v = edge - lines.append( - { - "start": vertex_xyz[u], - "end": vertex_xyz[v], - "color": self.edge_color[edge].rgb255, - "name": "{}.edge.{}-{}".format(self.mesh.name, u, v), - } - ) - return compas_ghpython.draw_lines(lines) - - def clear_edges(self): - """GH Artists are state-less. Therefore, clear does not have any effect. - Returns - ------- - None + for edge in edges or self.mesh.edges(): # type: ignore + lines.append(conversions.line_to_rhino((self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]))) - """ - pass + return lines + + def draw_faces(self, faces=None, color=None): + """Draw a selection of faces. - def clear_vertices(self): - """GH Artists are state-less. Therefore, clear does not have any effect. + Parameters + ---------- + faces : list[int], optional + A selection of faces to draw. + The default is None, in which case all faces are drawn. + color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color specification for the faces. Returns ------- - None + list[:rhino:`Rhino.Geometry.Mesh`] """ - pass + faces = faces or self.mesh.faces() # type: ignore - def clear_faces(self): - """GH Artists are state-less. Therefore, clear does not have any effect. + self.face_color = color - Returns - ------- - None + meshes = [] - """ - pass + for face in faces: + color = self.face_color[face] # type: ignore + vertices = [self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)] # type: ignore + facet = ngon(len(vertices)) + if facet: + meshes.append(conversions.vertices_and_faces_to_rhino(vertices, [facet])) + + return meshes diff --git a/src/compas_ghpython/artists/networkartist.py b/src/compas_ghpython/artists/networkartist.py index 1e298802752..cffb211564d 100644 --- a/src/compas_ghpython/artists/networkartist.py +++ b/src/compas_ghpython/artists/networkartist.py @@ -2,12 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import NetworkArtist +from compas_rhino import conversions + +from compas.artists import NetworkArtist as BaseArtist from .artist import GHArtist -class NetworkArtist(GHArtist, NetworkArtist): +class NetworkArtist(GHArtist, BaseArtist): """Artist for drawing network data structures. Parameters @@ -16,7 +17,6 @@ class NetworkArtist(GHArtist, NetworkArtist): A COMPAS network. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.NetworkArtist` for more info. """ @@ -36,7 +36,7 @@ def draw(self): """ return self.draw_nodes(), self.draw_edges() - def draw_nodes(self, nodes=None, color=None): + def draw_nodes(self, nodes=None): """Draw a selection of nodes. Parameters @@ -44,30 +44,20 @@ def draw_nodes(self, nodes=None, color=None): nodes: list[hashable], optional The selection of nodes that should be drawn. Default is None, in which case all nodes are drawn. - color: :class:`~compas.colors.Color` | dict[hashable, :class:`~compas.colors.Color`], optional - The color specification for the nodes. - The default color is :attr:`NetworkArtist.default_nodecolor`. Returns ------- list[:rhino:`Rhino.Geometry.Point3d`] """ - self.node_color = color - node_xyz = self.node_xyz - nodes = nodes or list(self.network.nodes()) points = [] - for node in nodes: - points.append( - { - "pos": node_xyz[node], - "name": "{}.node.{}".format(self.network.name, node), - "color": self.node_color[node].rgb255, - } - ) - return compas_ghpython.draw_points(points) - - def draw_edges(self, edges=None, color=None): + + for node in nodes or self.network.nodes(): # type: ignore + points.append(conversions.point_to_rhino(self.node_xyz[node])) + + return points + + def draw_edges(self, edges=None): """Draw a selection of edges. Parameters @@ -75,35 +65,15 @@ def draw_edges(self, edges=None, color=None): edges : list[tuple[hashable, hashable]], optional A list of edges to draw. The default is None, in which case all edges are drawn. - color : :class:`~compas.colors.Color` | dict[tuple[hashable, hashable], :class:`~compas.colors.Color`], optional - The color specification for the edges. - The default color is the value of :attr:`NetworkArtist.default_edgecolor`. Returns ------- list[:rhino:`Rhino.Geometry.Line`] """ - self.edge_color = color - node_xyz = self.node_xyz - edges = edges or list(self.network.edges()) lines = [] - for edge in edges: - u, v = edge - lines.append( - { - "start": node_xyz[u], - "end": node_xyz[v], - "color": self.edge_color[edge].rgb255, - "name": "{}.edge.{}-{}".format(self.network.name, u, v), - } - ) - return compas_ghpython.draw_lines(lines) - - def clear_edges(self): - """GH Artists are state-less. Therefore, clear does not have any effect.""" - pass - - def clear_nodes(self): - """GH Artists are state-less. Therefore, clear does not have any effect.""" - pass + + for edge in edges or self.network.edges(): # type: ignore + lines.append(conversions.line_to_rhino((self.node_xyz[edge[0]], self.node_xyz[edge[1]]))) + + return lines diff --git a/src/compas_ghpython/artists/pointartist.py b/src/compas_ghpython/artists/pointartist.py index 741f20dd0c7..98353c5c929 100644 --- a/src/compas_ghpython/artists/pointartist.py +++ b/src/compas_ghpython/artists/pointartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython.utilities -from compas.artists import PrimitiveArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class PointArtist(GHArtist, PrimitiveArtist): +class PointArtist(GHArtist, GeometryArtist): """Artist for drawing points. Parameters @@ -17,27 +17,23 @@ class PointArtist(GHArtist, PrimitiveArtist): A COMPAS point. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. """ def __init__(self, point, **kwargs): - super(PointArtist, self).__init__(primitive=point, **kwargs) + super(PointArtist, self).__init__(geometry=point, **kwargs) - def draw(self, color=None): + def draw(self): """Draw the point. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the point. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - Returns ------- :rhino:`Rhino.Geometry.Point3d` """ - color = Color.coerce(color) or self.color - points = [{"pos": list(self.primitive), "color": color.rgb255}] - return compas_ghpython.utilities.draw_points(points)[0] + geometry = conversions.point_to_rhino(self.geometry) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/polygonartist.py b/src/compas_ghpython/artists/polygonartist.py index 8b8f00575e6..bf194c53e2d 100644 --- a/src/compas_ghpython/artists/polygonartist.py +++ b/src/compas_ghpython/artists/polygonartist.py @@ -2,13 +2,15 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import PrimitiveArtist from compas.colors import Color + +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class PolygonArtist(GHArtist, PrimitiveArtist): +class PolygonArtist(GHArtist, GeometryArtist): """Artist for drawing polygons. Parameters @@ -17,53 +19,32 @@ class PolygonArtist(GHArtist, PrimitiveArtist): A COMPAS polygon. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. """ def __init__(self, polygon, **kwargs): - super(PolygonArtist, self).__init__(primitive=polygon, **kwargs) + super(PolygonArtist, self).__init__(geometry=polygon, **kwargs) - def draw(self, color=None, show_points=False, show_edges=False, show_face=True): + def draw(self, color=None, show_vertices=False, show_edges=False): """Draw the polygon. Parameters ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the polygon. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - show_points : bool, optional - If True, draw the points of the polygon. - show_edges : bool, optional - If True, draw the edges of the polygon. - show_face : bool, optional - If True, draw the face of the polygon. Returns ------- - list[:rhino:`Rhino.Geometry.Point3d`, :rhino:`Rhino.Geometry.Line`, :rhino:`Rhino.Geometry.Mesh`] - The Rhino points, lines and face. + :rhino:`Rhino.Geometry.Mesh` """ color = Color.coerce(color) or self.color - color = color.rgb255 - _points = map(list, self.primitive.points) - result = [] - if show_points: - points = [{"pos": point, "color": color, "name": self.primitive.name} for point in _points] - result += compas_ghpython.draw_points(points) - if show_edges: - lines = [ - { - "start": list(a), - "end": list(b), - "color": color, - "name": self.primitive.name, - } - for a, b in self.primitive.lines - ] - result += compas_ghpython.draw_lines(lines) - if show_face: - polygons = [{"points": _points, "color": color, "name": self.primitive.name}] - result += compas_ghpython.draw_faces(polygons) - return result + vertices = self.geometry.vertices + faces = self.geometry.faces + + geometry = conversions.vertices_and_faces_to_rhino(vertices, faces, color=color) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/polyhedronartist.py b/src/compas_ghpython/artists/polyhedronartist.py index c4d668d71df..b140665fa5b 100644 --- a/src/compas_ghpython/artists/polyhedronartist.py +++ b/src/compas_ghpython/artists/polyhedronartist.py @@ -2,13 +2,15 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import ShapeArtist from compas.colors import Color + +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class PolyhedronArtist(GHArtist, ShapeArtist): +class PolyhedronArtist(GHArtist, GeometryArtist): """Artist for drawing polyhedron shapes. Parameters @@ -17,12 +19,11 @@ class PolyhedronArtist(GHArtist, ShapeArtist): A COMPAS polyhedron. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.ShapeArtist` for more info. """ def __init__(self, polyhedron, **kwargs): - super(PolyhedronArtist, self).__init__(shape=polyhedron, **kwargs) + super(PolyhedronArtist, self).__init__(geometry=polyhedron, **kwargs) def draw(self, color=None): """Draw the polyhedron associated with the artist. @@ -31,7 +32,6 @@ def draw(self, color=None): ---------- color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional The RGB color of the line. - Default is :attr:`compas.artists.ShapeArtist.color`. Returns ------- @@ -39,7 +39,11 @@ def draw(self, color=None): """ color = Color.coerce(color) or self.color - vertices = [list(vertex) for vertex in self.shape.vertices] - faces = self.shape.faces - mesh = compas_ghpython.draw_mesh(vertices, faces, color=color.rgb255) - return mesh + vertices, faces = self.geometry.to_vertices_and_faces() + + geometry = conversions.vertices_and_faces_to_rhino(vertices, faces, color=color) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/polylineartist.py b/src/compas_ghpython/artists/polylineartist.py index 3aa59988623..01854df0651 100644 --- a/src/compas_ghpython/artists/polylineartist.py +++ b/src/compas_ghpython/artists/polylineartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import PrimitiveArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class PolylineArtist(GHArtist, PrimitiveArtist): +class PolylineArtist(GHArtist, GeometryArtist): """Artist for drawing polylines. Parameters @@ -17,27 +17,23 @@ class PolylineArtist(GHArtist, PrimitiveArtist): A COMPAS polyline. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. """ def __init__(self, polyline, **kwargs): - super(PolylineArtist, self).__init__(primitive=polyline, **kwargs) + super(PolylineArtist, self).__init__(geometry=polyline, **kwargs) - def draw(self, color=None): + def draw(self): """Draw the polyline. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the polyline. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - Returns ------- - :rhino:`Rhino.Geometry.Polyline`. + :rhino:`Rhino.Geometry.PolylineCurve`. """ - color = Color.coerce(color) or self.color - polylines = [{"points": map(list, self.primitive.points), "color": color.rgb255}] - return compas_ghpython.draw_polylines(polylines)[0] + geometry = conversions.polyline_to_rhino_curve(self.geometry) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/robotmodelartist.py b/src/compas_ghpython/artists/robotmodelartist.py index c5b1753096b..913ec110e19 100644 --- a/src/compas_ghpython/artists/robotmodelartist.py +++ b/src/compas_ghpython/artists/robotmodelartist.py @@ -5,7 +5,7 @@ import compas_ghpython from compas.utilities import rgb_to_rgb -from compas_rhino.geometry.transformations import xtransform +# from compas_rhino.geometry.transformations import xtransform from compas.artists import RobotModelArtist from .artist import GHArtist @@ -27,9 +27,9 @@ class RobotModelArtist(GHArtist, RobotModelArtist): def __init__(self, model, **kwargs): super(RobotModelArtist, self).__init__(model=model, **kwargs) - # again not really sure why this is here - def transform(self, native_mesh, transformation): - xtransform(native_mesh, transformation) + # # again not really sure why this is here + # def transform(self, native_mesh, transformation): + # xtransform(native_mesh, transformation) # same here # there is no reference to self... diff --git a/src/compas_ghpython/artists/sphereartist.py b/src/compas_ghpython/artists/sphereartist.py index 3ec8cbe4dbe..17ba29ea2db 100644 --- a/src/compas_ghpython/artists/sphereartist.py +++ b/src/compas_ghpython/artists/sphereartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import ShapeArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class SphereArtist(GHArtist, ShapeArtist): +class SphereArtist(GHArtist, GeometryArtist): """Artist for drawing sphere shapes. Parameters @@ -17,37 +17,23 @@ class SphereArtist(GHArtist, ShapeArtist): A COMPAS sphere. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.ShapeArtist` for more info. """ def __init__(self, sphere, **kwargs): - super(SphereArtist, self).__init__(shape=sphere, **kwargs) + super(SphereArtist, self).__init__(geometry=sphere, **kwargs) - def draw(self, color=None, u=None, v=None): + def draw(self): """Draw the sphere associated with the artist. - Parameters - ---------- - color : tuple[int, int, int] | tuple[flot, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the sphere. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`SphereArtist.u` - v : int, optional - Number of faces in the "v" direction. - Default is :attr:`SphereArtist.v`. - Returns ------- - :rhino:`Rhino.Geometry.Mesh` + :rhino:`Rhino.Geometry.Sphere` """ - color = Color.coerce(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] - mesh = compas_ghpython.draw_mesh(vertices, faces, color=color.rgb255) - return mesh + geometry = conversions.sphere_to_rhino(self.geometry) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/surfaceartist.py b/src/compas_ghpython/artists/surfaceartist.py index e1a01ca85fa..d786a78c53a 100644 --- a/src/compas_ghpython/artists/surfaceartist.py +++ b/src/compas_ghpython/artists/surfaceartist.py @@ -2,11 +2,13 @@ from __future__ import absolute_import from __future__ import division -from compas.artists import SurfaceArtist +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class SurfaceArtist(GHArtist, SurfaceArtist): +class SurfaceArtist(GHArtist, GeometryArtist): """Artist for drawing surfaces. Parameters @@ -18,12 +20,11 @@ class SurfaceArtist(GHArtist, SurfaceArtist): ---------------- **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`GHArtist` and :class:`~compas.artists.SurfaceArtist`. """ def __init__(self, surface, **kwargs): - super(SurfaceArtist, self).__init__(surface=surface, **kwargs) + super(SurfaceArtist, self).__init__(geometry=surface, **kwargs) def draw(self): """Draw the surface. @@ -33,4 +34,9 @@ def draw(self): :rhino:`Rhino.Geometry.Surface` """ - return self.surface.rhino_surface + geometry = conversions.surface_to_rhino(self.geometry) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/torusartist.py b/src/compas_ghpython/artists/torusartist.py index 737d773d5fb..bd3016b001a 100644 --- a/src/compas_ghpython/artists/torusartist.py +++ b/src/compas_ghpython/artists/torusartist.py @@ -2,13 +2,13 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import ShapeArtist -from compas.colors import Color +from compas_rhino import conversions + +from compas.artists import GeometryArtist from .artist import GHArtist -class TorusArtist(GHArtist, ShapeArtist): +class TorusArtist(GHArtist, GeometryArtist): """Artist for drawing torus shapes. Parameters @@ -17,37 +17,23 @@ class TorusArtist(GHArtist, ShapeArtist): A COMPAS torus. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.ShapeArtist` for more info. """ def __init__(self, torus, **kwargs): - super(TorusArtist, self).__init__(shape=torus, **kwargs) + super(TorusArtist, self).__init__(geometry=torus, **kwargs) - def draw(self, color=None, u=None, v=None): + def draw(self): """Draw the torus associated with the artist. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the torus. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`TorusArtist.u` - v : int, optional - Number of faces in the "v" direction. - Default is :attr:`TorusArtist.v`. - Returns ------- - :rhino:`Rhino.Geometry.Mesh` + :rhino:`Rhino.Geometry.Torus` """ - color = Color.coerce(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] - mesh = compas_ghpython.draw_mesh(vertices, faces, color=color.rgb255) - return mesh + geometry = conversions.torus_to_rhino(self.geometry) + + if self.transformation: + geometry.Transform(conversions.transformation_to_rhino(self.transformation)) + + return geometry diff --git a/src/compas_ghpython/artists/vectorartist.py b/src/compas_ghpython/artists/vectorartist.py index 1bdfb481466..09fc409f811 100644 --- a/src/compas_ghpython/artists/vectorartist.py +++ b/src/compas_ghpython/artists/vectorartist.py @@ -2,15 +2,14 @@ from __future__ import division from __future__ import print_function -import compas_ghpython -from compas.artists import PrimitiveArtist -from compas.geometry import Point -from compas.colors import Color +from compas_rhino import conversions +from compas.geometry import Point +from compas.artists import GeometryArtist from .artist import GHArtist -class VectorArtist(GHArtist, PrimitiveArtist): +class VectorArtist(GHArtist, GeometryArtist): """Artist for drawing vectors. Parameters @@ -19,52 +18,34 @@ class VectorArtist(GHArtist, PrimitiveArtist): A COMPAS vector. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. """ def __init__(self, vector, **kwargs): - super(VectorArtist, self).__init__(primitive=vector, **kwargs) + super(VectorArtist, self).__init__(geometry=vector, **kwargs) - def draw(self, color=None, point=None, show_point=False): + def draw(self, point=None, show_point=False): """Draw the vector. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional - The RGB color of the vector. - Default is :attr:`compas.artists.PrimitiveArtist.color`. point : [float, float, float] | :class:`~compas.geometry.Point`, optional Point of application of the vector. Default is ``Point(0, 0, 0)``. - show_point : bool, optional - If True, draw the point of application of the vector. Returns ------- - list[:rhino:`Rhino.Geometry.Point3d`, :rhino:`Rhino.Geometry.Line`] - The Rhino line and endpoints, if requested. + :rhino:`Rhino.Geometry.Line` """ - color = Color.coerce(color) or self.color - color = color.rgb255 point = point or [0, 0, 0] start = Point(*point) - end = start + self.primitive - start = list(start) - end = list(end) - result = [] - if show_point: - points = [{"pos": start, "color": color, "name": self.primitive.name}] - result += compas_ghpython.draw_points(points) - lines = [ - { - "start": start, - "end": end, - "arrow": "end", - "color": color, - "name": self.primitive.name, - } - ] - result += compas_ghpython.draw_lines(lines) - return result + end = start + self.geometry + + geometry = conversions.line_to_rhino([start, end]) + + if self.transformation: + transformation = conversions.transformation_to_rhino(self.transformation) + geometry.Transform(transformation) + + return geometry diff --git a/src/compas_ghpython/artists/volmeshartist.py b/src/compas_ghpython/artists/volmeshartist.py index db4065982e7..6ef243e9777 100644 --- a/src/compas_ghpython/artists/volmeshartist.py +++ b/src/compas_ghpython/artists/volmeshartist.py @@ -2,12 +2,14 @@ from __future__ import absolute_import from __future__ import division -import compas_ghpython -from compas.artists import VolMeshArtist +from compas_rhino import conversions +from compas_rhino.artists._helpers import ngon + +from compas.artists import VolMeshArtist as BaseArtist from .artist import GHArtist -class VolMeshArtist(GHArtist, VolMeshArtist): +class VolMeshArtist(GHArtist, BaseArtist): """Artist for drawing volmesh data structures. Parameters @@ -16,7 +18,6 @@ class VolMeshArtist(GHArtist, VolMeshArtist): A COMPAS volmesh. **kwargs : dict, optional Additional keyword arguments. - See :class:`~compas_ghpython.artists.GHArtist` and :class:`~compas.artists.VolMeshArtist` for more info. """ @@ -44,7 +45,7 @@ def draw(self, cells=None, color=None): """ return self.draw_cells(cells=cells, color=color) - def draw_vertices(self, vertices=None, color=None): + def draw_vertices(self, vertices=None): """Draw a selection of vertices. Parameters @@ -52,30 +53,20 @@ def draw_vertices(self, vertices=None, color=None): vertices : list A list of vertices to draw. Default is None, in which case all vertices are drawn. - color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`] - The color specification for the vertices. - The default color of the vertices is :attr:`VolMeshArtist.default_vertexcolor`. Returns ------- list[:rhino:`Rhino.Geometry.Point3d`] """ - self.vertex_color = color - vertices = vertices or self.vertices - vertex_xyz = self.vertex_xyz points = [] - for vertex in vertices: - points.append( - { - "pos": vertex_xyz[vertex], - "name": "{}.vertex.{}".format(self.volmesh.name, vertex), - "color": self.vertex_color[vertex].rgb255, - } - ) - return compas_ghpython.draw_points(points) - - def draw_edges(self, edges=None, color=None): + + for vertex in vertices or self.volmesh.vertices(): # type: ignore + points.append(conversions.point_to_rhino(self.vertex_xyz[vertex])) + + return points + + def draw_edges(self, edges=None): """Draw a selection of edges. Parameters @@ -83,32 +74,20 @@ def draw_edges(self, edges=None, color=None): edges : list[tuple[int, int]], optional A list of edges to draw. The default is None, in which case all edges are drawn. - color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional - The color specification for the edges. - The default color is :attr:`VolMeshArtist.default_edgecolor`. Returns ------- list[:rhino:`Rhino.Geometry.Line`] """ - self.edge_color = color - edges = edges or self.edges - vertex_xyz = self.vertex_xyz lines = [] - for edge in edges: - u, v = edge - lines.append( - { - "start": vertex_xyz[u], - "end": vertex_xyz[v], - "color": self.edge_color[edge].rgb255, - "name": "{}.edge.{}-{}".format(self.volmesh.name, u, v), - } - ) - return compas_ghpython.draw_lines(lines) - - def draw_faces(self, faces=None, color=None, join_faces=False): + + for edge in edges or self.volmesh.edges(): # type: ignore + lines.append(conversions.line_to_rhino((self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]))) + + return lines + + def draw_faces(self, faces=None, color=None): """Draw a selection of faces. Parameters @@ -119,27 +98,26 @@ def draw_faces(self, faces=None, color=None, join_faces=False): color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color specification for the faces. The default color is :attr:`VolMeshArtist.default_facecolor`. - join_faces : bool, optional - If True, join the faces into one mesh. Returns ------- list[:rhino:`Rhino.Geometry.Mesh`] """ + faces = faces or self.volmesh.faces() # type: ignore + self.face_color = color - faces = faces or self.faces - vertex_xyz = self.vertex_xyz - facets = [] + + meshes = [] + 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": self.face_color[face].rgb255, - } - ) - return compas_ghpython.draw_faces(facets) + color = self.face_color[face] # type: ignore + vertices = [self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)] # type: ignore + facet = ngon(len(vertices)) + if facet: + meshes.append(conversions.vertices_and_faces_to_rhino(vertices, [facet])) + + return meshes def draw_cells(self, cells=None, color=None): """Draw a selection of cells. @@ -151,7 +129,6 @@ def draw_cells(self, cells=None, color=None): The default is None, in which case all cells are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the cells. - The default color is :attr:`VolMeshArtist.default_cellcolor`. Returns ------- @@ -159,95 +136,19 @@ def draw_cells(self, cells=None, color=None): """ self.cell_color = color - cells = cells or self.cells - vertex_xyz = self.vertex_xyz - meshes = [] - for cell in cells: - vertices = self.volmesh.cell_vertices(cell) - faces = self.volmesh.cell_faces(cell) - vertex_index = dict((vertex, index) for index, vertex in enumerate(vertices)) - vertices = [vertex_xyz[vertex] for vertex in vertices] - faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] - mesh = compas_ghpython.draw_mesh(vertices, faces, color=self.cell_color[cell].rgb255) - meshes.append(mesh) - return meshes - - def clear_vertices(self): - """GH Artists are state-less. Therefore, clear does not have any effect. - - Returns - ------- - None - - """ - pass - - def clear_edges(self): - """GH Artists are state-less. Therefore, clear does not have any effect. - Returns - ------- - None - - """ - pass - - def clear_faces(self): - """GH Artists are state-less. Therefore, clear does not have any effect. - - Returns - ------- - None - - """ - pass - - def clear_cells(self): - """GH Artists are state-less. Therefore, clear does not have any effect. - - Returns - ------- - None - - """ - pass - - def clear_vertexlabels(self): - """GH Artists are state-less. Therefore, clear does not have any effect. - - Returns - ------- - None - - """ - pass - - def clear_edgelabels(self): - """GH Artists are state-less. Therefore, clear does not have any effect. - - Returns - ------- - None - - """ - pass - - def clear_facelabels(self): - """GH Artists are state-less. Therefore, clear does not have any effect. - - Returns - ------- - None + meshes = [] - """ - pass + for cell in cells or self.volmesh.cells(): # type: ignore + color = self.cell_color[cell] # type: ignore - def clear_celllabels(self): - """GH Artists are state-less. Therefore, clear does not have any effect. + vertices = self.volmesh.cell_vertices(cell) # type: ignore + faces = self.volmesh.cell_faces(cell) # type: ignore + vertex_index = dict((vertex, index) for index, vertex in enumerate(vertices)) + vertices = [self.vertex_xyz[vertex] for vertex in vertices] + faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] # type: ignore - Returns - ------- - None + mesh = conversions.vertices_and_faces_to_rhino(vertices, faces, disjoint=True) + meshes.append(mesh) - """ - pass + return meshes diff --git a/src/compas_ghpython/conversions/__init__.py b/src/compas_ghpython/conversions/__init__.py new file mode 100644 index 00000000000..06fd6602255 --- /dev/null +++ b/src/compas_ghpython/conversions/__init__.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from .meshes import mesh_to_rhino +from .meshes import vertices_and_faces_to_rhino_mesh + +__all__ = [ + "mesh_to_rhino", + "vertices_and_faces_to_rhino_mesh", +] diff --git a/src/compas_ghpython/conversions/meshes.py b/src/compas_ghpython/conversions/meshes.py new file mode 100644 index 00000000000..0301b03a368 --- /dev/null +++ b/src/compas_ghpython/conversions/meshes.py @@ -0,0 +1,105 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import rhinoscriptsyntax as rs # type: ignore + +from Rhino.Geometry import Mesh as RhinoMesh # type: ignore +from Rhino.Geometry import Point2f # type: ignore +from Rhino.Geometry import Vector3f # type: ignore + +from System.Array import CreateInstance # type: ignore +from System.Drawing import Color # type: ignore + +try: + from Rhino.Geometry import MeshNgon # type: ignore +except ImportError: + MeshNgon = False + +from compas.geometry import centroid_points +from compas.utilities import pairwise + + +def vertices_and_faces_to_rhino_mesh(vertices, faces, color=None, vertex_normals=None, texture_coordinates=None): + """Convert a list of vertices and faces to a Rhino mesh. + + Parameters + ---------- + vertices : list + A list of vertex coordinates. + faces : list + A list of faces, with each face defined as a list of indices into the list of vertices. + + Returns + ------- + :rhino:`Rhino.Geometry.Mesh` + + """ + rmesh = RhinoMesh() + + for a, b, c in vertices: + rmesh.Vertices.Add(a, b, c) + + for face in faces: + f = len(face) + if f < 3: + continue + if f == 3: + rmesh.Faces.AddFace(*face) + elif f == 4: + rmesh.Faces.AddFace(*face) + else: + if MeshNgon: + centroid = centroid_points([vertices[index] for index in face]) + c = rmesh.Vertices.Add(*centroid) + facets = [] + for i, j in pairwise(face + face[:1]): + facets.append(rmesh.Faces.AddFace(i, j, c)) + ngon = MeshNgon.Create(face, facets) # type: ignore + rmesh.Ngons.AddNgon(ngon) + + if vertex_normals: + count = len(vertex_normals) + normals = CreateInstance(Vector3f, count) + for i, normal in enumerate(vertex_normals): + normals[i] = Vector3f(normal[0], normal[1], normal[2]) + rmesh.Normals.SetNormals(normals) + + if texture_coordinates: + count = len(texture_coordinates) + tcs = CreateInstance(Point2f, count) + for i, tc in enumerate(texture_coordinates): + tcs[i] = Point2f(tc[0], tc[1]) + rmesh.TextureCoordinates.SetTextureCoordinates(tcs) + + if color: + count = len(rmesh.Vertices) + colors = CreateInstance(Color, count) + for i in range(count): + colors[i] = rs.coercecolor(color) + rmesh.VertexColors.SetColors(colors) + + return rmesh + + +def mesh_to_rhino(mesh, color=None, vertex_normals=None, texture_coordinates=None): + """Convert a COMPAS mesh to a Rhino mesh. + + Parameters + ---------- + mesh : :class:`compas.datastructures.Mesh` + A COMPAS mesh. + + Returns + ------- + :class:`Rhino.Geometry.Mesh` + + """ + vertices, faces = mesh.to_vertices_and_faces() + return vertices_and_faces_to_rhino_mesh( + vertices, + faces, + color=color, + vertex_normals=vertex_normals, + texture_coordinates=texture_coordinates, + ) diff --git a/src/compas_plotters/__init__.py b/src/compas_plotters/__init__.py deleted file mode 100644 index f7c5f5544ae..00000000000 --- a/src/compas_plotters/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -******************************************************************************** -compas_plotters -******************************************************************************** - -.. currentmodule:: compas_plotters - -.. toctree:: - :maxdepth: 1 - :titlesonly: - - compas_plotters.artists - compas_plotters.plotter - -""" - -__version__ = "1.17.5" - -from .core import * # noqa: F401 F403 - -# from .artists import * # noqa: F401 F403 -from .plotter import Plotter - - -__all__ = ["Plotter"] - -__all_plugins__ = [ - "compas_plotters.artists", -] diff --git a/src/compas_plotters/artists/__init__.py b/src/compas_plotters/artists/__init__.py deleted file mode 100644 index 941a5838d7f..00000000000 --- a/src/compas_plotters/artists/__init__.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -******************************************************************************** -artists -******************************************************************************** - -.. currentmodule:: compas_plotters.artists - - -Primitive Artists -================= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - PointArtist - VectorArtist - LineArtist - PolylineArtist - PolygonArtist - CircleArtist - EllipseArtist - - -Datastructure Artists -===================== - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - MeshArtist - NetworkArtist - - -Base Classes -============ - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - PlotterArtist - -""" - -from compas.plugins import plugin -from compas.artists import Artist - -from compas.geometry import Point -from compas.geometry import Vector -from compas.geometry import Line -from compas.geometry import Polyline -from compas.geometry import Polygon -from compas.geometry import Circle -from compas.geometry import Ellipse - -from compas.datastructures import Mesh -from compas.datastructures import Network - -from .artist import PlotterArtist -from .pointartist import PointArtist -from .vectorartist import VectorArtist -from .lineartist import LineArtist -from .polylineartist import PolylineArtist -from .polygonartist import PolygonArtist -from .circleartist import CircleArtist -from .ellipseartist import EllipseArtist -from .meshartist import MeshArtist -from .networkartist import NetworkArtist - - -@plugin(category="factories", requires=["matplotlib"]) -def register_artists(): - Artist.register(Point, PointArtist, context="Plotter") - Artist.register(Vector, VectorArtist, context="Plotter") - Artist.register(Line, LineArtist, context="Plotter") - Artist.register(Polyline, PolylineArtist, context="Plotter") - Artist.register(Polygon, PolygonArtist, context="Plotter") - Artist.register(Circle, CircleArtist, context="Plotter") - Artist.register(Ellipse, EllipseArtist, context="Plotter") - Artist.register(Mesh, MeshArtist, context="Plotter") - Artist.register(Network, NetworkArtist, context="Plotter") - - -__all__ = [ - "PlotterArtist", - "PointArtist", - "VectorArtist", - "LineArtist", - "PolylineArtist", - "PolygonArtist", - "CircleArtist", - "EllipseArtist", - "MeshArtist", - "NetworkArtist", -] diff --git a/src/compas_plotters/artists/artist.py b/src/compas_plotters/artists/artist.py deleted file mode 100644 index 961534a5474..00000000000 --- a/src/compas_plotters/artists/artist.py +++ /dev/null @@ -1,56 +0,0 @@ -from abc import abstractproperty - -from compas.artists import Artist - - -class PlotterArtist(Artist): - """Base class for all plotter artists. - - Attributes - ---------- - plotter : :class:`~compas_plotters.plotter.Plotter`, read-only - A plotter instance. - data : list[[float, float]] - The geometrical data points visualized with the plotter. - - """ - - def __init__(self, plotter, **kwargs): - super().__init__(**kwargs) - self._plotter = plotter - - @property - def plotter(self): - # if not self._plotter: - # from compas_plotters import Plotter - # self._plotter = Plotter() - return self._plotter - - @abstractproperty - def data(self): - raise NotImplementedError - - def viewbox(self): - """Compute the bounds of the current view. - - Returns - ------- - tuple[[float, float], [float, float], [float, float], [float]] - Coordinates of the corners of the 2D view box. - - """ - xlim = self.plotter.axes.get_xlim() - ylim = self.plotter.axes.get_ylim() - xmin, xmax = xlim - ymin, ymax = ylim - return [xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax] - - def update_data(self) -> None: - """Update the data limits of the plotting axes using the visualization data. - - Returns - ------- - None - - """ - self.plotter.axes.update_datalim(self.data) diff --git a/src/compas_plotters/artists/circleartist.py b/src/compas_plotters/artists/circleartist.py deleted file mode 100644 index 12bf048f1e6..00000000000 --- a/src/compas_plotters/artists/circleartist.py +++ /dev/null @@ -1,127 +0,0 @@ -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.artists import PrimitiveArtist -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class CircleArtist(PlotterArtist, PrimitiveArtist): - """Artist for COMPAS circles. - - Parameters - ---------- - circle : :class:`~compas.geometry.Circle` - A COMPAS circle. - linewidth : float, optional - Width of the circle boundary. - linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional - Style of the circle boundary. - facecolor : tuple[float ,float, float], optional - Color of the interior of the circle. - edgecolor : tuple[float, float, float], optional - Color of the boundary of the circle. - fill : bool, optional - If True, draw the interior of the circle. - alpha : float, optional - Transparency of the circle. - zorder : int, optional - Stacking order of the circle on the canvas. - **kwargs : dict, optional - Additional keyword arguments. - See :class:`~compas_plotters.artists.PlotterArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. - - Attributes - ---------- - circle : :class:`~compas.geometry.Circle` - The circle associated with the artist. - - """ - - def __init__( - self, - circle: Circle, - linewidth: float = 1.0, - linestyle: Literal["solid", "dotted", "dashed", "dashdot"] = "solid", - facecolor: Color = (1.0, 1.0, 1.0), - edgecolor: Color = (0, 0, 0), - fill: bool = True, - alpha: float = 1.0, - zorder: int = 1000, - **kwargs: Any - ): - - super().__init__(primitive=circle, **kwargs) - - self._mpl_circle = None - self.linewidth = linewidth - self.linestyle = linestyle - self.facecolor = facecolor - self.edgecolor = edgecolor - self.fill = fill - self.alpha = alpha - self.zorder = zorder - - @property - def circle(self): - return self.primitive - - @circle.setter - def circle(self, circle): - self.primitive = circle - - @property - def data(self) -> List[List[float]]: - points = [ - self.circle.center[:2], - self.circle.center[:2], - self.circle.center[:2], - self.circle.center[:2], - ] - points[0][0] -= self.circle.radius - points[1][0] += self.circle.radius - points[2][1] -= self.circle.radius - points[3][1] += self.circle.radius - return points - - def draw(self) -> None: - """Draw the circle on the plotter canvas. - - Returns - ------- - None - - """ - circle = CirclePatch( - self.circle.center[:2], - linewidth=self.linewidth, - linestyle=self.linestyle, - radius=self.circle.radius, - facecolor=self.facecolor, - edgecolor=self.edgecolor, - fill=self.fill, - alpha=self.alpha, - zorder=self.zorder, - ) - self._mpl_circle = self.plotter.axes.add_artist(circle) - self.update_data() - - def redraw(self) -> None: - """Update the circle using the current geometry and visualization settings. - - Returns - ------- - None - - """ - self._mpl_circle.center = self.circle.center[:2] - self._mpl_circle.set_radius(self.circle.radius) - self._mpl_circle.set_edgecolor(self.edgecolor) - self._mpl_circle.set_facecolor(self.facecolor) - self.update_data() diff --git a/src/compas_plotters/artists/ellipseartist.py b/src/compas_plotters/artists/ellipseartist.py deleted file mode 100644 index 075a2fb6c4c..00000000000 --- a/src/compas_plotters/artists/ellipseartist.py +++ /dev/null @@ -1,125 +0,0 @@ -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.artists import PrimitiveArtist -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class EllipseArtist(PlotterArtist, PrimitiveArtist): - """Artist for COMPAS ellipses. - - Parameters - ---------- - ellipse : :class:`~compas.geometry.Ellipse` - A COMPAS ellipse. - linewidth : float, optional - Width of the ellipse boundary. - linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional - Style of the ellipse boundary. - facecolor : tuple[float ,float, float], optional - Color of the interior of the ellipse. - edgecolor : tuple[float, float, float], optional - Color of the boundary of the ellipse. - fill : bool, optional - If True, draw the interior of the ellipse. - alpha : float, optional - Transparency of the ellipse. - zorder : int, optional - Stacking order of the ellipse on the canvas. - **kwargs : dict, optional - Additional keyword arguments. - See :class:`~compas_plotters.artists.PlotterArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. - - Attributes - ---------- - ellipse : :class:`~compas.geometry.Ellipse` - The ellipse associated with the artist. - - """ - - def __init__( - self, - ellipse: Ellipse, - linewidth: float = 1.0, - linestyle: Literal["solid", "dotted", "dashed", "dashdot"] = "solid", - facecolor: Color = (1.0, 1.0, 1.0), - edgecolor: Color = (0, 0, 0), - fill: bool = True, - alpha: float = 1.0, - zorder: int = 1000, - **kwargs: Any - ): - - super().__init__(primitive=ellipse, **kwargs) - - self._mpl_ellipse = None - self.linewidth = linewidth - self.linestyle = linestyle - self.facecolor = facecolor - self.edgecolor = edgecolor - self.fill = fill - self.alpha = alpha - self.zorder = zorder - - @property - def ellipse(self): - return self.primitive - - @ellipse.setter - def ellipse(self, ellipse): - self.primitive = ellipse - - @property - def data(self) -> List[List[float]]: - points = [ - self.ellipse.center[:2], - self.ellipse.center[:2], - self.ellipse.center[:2], - self.ellipse.center[:2], - ] - points[0][0] -= self.ellipse.major - points[1][0] += self.ellipse.major - points[2][1] -= self.ellipse.minor - points[3][1] += self.ellipse.minor - return points - - def draw(self) -> None: - """Draw the ellipse on the plotter canvas. - - Returns - ------- - None - - """ - ellipse = EllipsePatch( - self.ellipse.center[:2], - width=2 * self.ellipse.major, - height=2 * self.ellipse.minor, - facecolor=self.facecolor, - edgecolor=self.edgecolor, - fill=self.fill, - alpha=self.alpha, - zorder=self.zorder, - ) - self._mpl_ellipse = self.plotter.axes.add_artist(ellipse) - - def redraw(self) -> None: - """Update the ellipse using the current geometry and visualization settings. - - Returns - ------- - None - - """ - self._mpl_ellipse.center = self.ellipse.center[:2] - self._mpl_ellipse.set_width(2 * self.ellipse.major) - self._mpl_ellipse.set_height(2 * self.ellipse.minor) - self._mpl_ellipse.set_edgecolor(self.edgecolor) - self._mpl_ellipse.set_facecolor(self.facecolor) diff --git a/src/compas_plotters/artists/lineartist.py b/src/compas_plotters/artists/lineartist.py deleted file mode 100644 index 431400b760d..00000000000 --- a/src/compas_plotters/artists/lineartist.py +++ /dev/null @@ -1,167 +0,0 @@ -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.artists import PrimitiveArtist -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class LineArtist(PlotterArtist, PrimitiveArtist): - """Artist for COMPAS lines. - - Parameters - ---------- - line : :class:`~compas.geometry.Line` - A COMPAS line. - draw_points : bool, optional - If True, draw the start and end point of the line. - draw_as_segment : bool, optional - If True, draw only the segment between start and end, instead of the infinite line. - linewidth : float, optional - Width of the line. - linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional - Style of the line. - color : tuple[float, float, float], optional - Color of the line. - zorder : int, optional - Stacking order of the line on the canvas. - **kwargs : dict, optional - Additional keyword arguments. - See :class:`~compas_plotters.artists.PlotterArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. - - Attributes - ---------- - line : :class:`~compas.geometry.Line` - The line associated with the artist. - - """ - - def __init__( - self, - line: Line, - draw_points: bool = False, - draw_as_segment: bool = False, - linewidth: float = 1.0, - linestyle: Literal["solid", "dotted", "dashed", "dashdot"] = "solid", - color: Color = (0, 0, 0), - zorder: int = 1000, - **kwargs: Any - ): - - super().__init__(primitive=line, **kwargs) - - self._mpl_line = None - self._start_artist = None - self._end_artist = None - self._segment_artist = None - self.draw_points = draw_points - self.draw_as_segment = draw_as_segment - self.linewidth = linewidth - self.linestyle = linestyle - self.color = color - self.zorder = zorder - - @property - def line(self): - return self.primitive - - @line.setter - def line(self, line): - self.primitive = line - - @property - def data(self) -> List[List[float]]: - return [self.line.start[:2], self.line.end[:2]] - - def clip(self) -> List[Point]: - """Compute the clipping points of the line for the current view box. - - Returns - ------- - list[[float, float, float]] - The intersection between the line and the viewbox. - - """ - xlim, ylim = self.plotter.viewbox - xmin, xmax = xlim - ymin, ymax = ylim - box = [[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]] - return intersection_line_box_xy(self.line, box) - - def draw(self) -> None: - """Draw the line associated with the artist. - - Returns - ------- - None - - """ - if self.draw_as_segment: - x0, y0 = self.line.start[:2] - x1, y1 = self.line.end[:2] - line2d = Line2D( - [x0, x1], - [y0, y1], - linewidth=self.linewidth, - linestyle=self.linestyle, - color=self.color, - zorder=self.zorder, - ) - self._mpl_line = self.plotter.axes.add_line(line2d) - if self.draw_points: - self._start_artist = self.plotter.add(self.line.start) - self._end_artist = self.plotter.add(self.line.end) - else: - points = self.clip() - if points: - p0, p1 = points - x0, y0 = p0[:2] - x1, y1 = p1[:2] - line2d = Line2D( - [x0, x1], - [y0, y1], - linewidth=self.linewidth, - linestyle=self.linestyle, - color=self.color, - zorder=self.zorder, - ) - self._mpl_line = self.plotter.axes.add_line(line2d) - if self.draw_points: - self._start_artist = self.plotter.add(self.line.start, edgecolor=self.color) - self._end_artist = self.plotter.add(self.line.end, edgecolor=self.color) - - def redraw(self) -> None: - """Update the line using the current geometry and visualization settings. - - Returns - ------- - None - - """ - if self.draw_as_segment: - x0, y0 = self.line.start[:2] - x1, y1 = self.line.end[:2] - self._mpl_line.set_xdata([x0, x1]) - self._mpl_line.set_ydata([y0, y1]) - self._mpl_line.set_color(self.color) - self._mpl_line.set_linewidth(self.linewidth) - else: - points = self.clip() - if points: - p0, p1 = points - x0, y0 = p0[:2] - x1, y1 = p1[:2] - self._mpl_line.set_xdata([x0, x1]) - self._mpl_line.set_ydata([y0, y1]) - self._mpl_line.set_color(self.color) - self._mpl_line.set_linewidth(self.linewidth) - if self.draw_points: - self._start_artist.redraw() - self._end_artist.redraw() diff --git a/src/compas_plotters/artists/meshartist.py b/src/compas_plotters/artists/meshartist.py deleted file mode 100644 index 1513df7ec72..00000000000 --- a/src/compas_plotters/artists/meshartist.py +++ /dev/null @@ -1,733 +0,0 @@ -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.geometry import centroid_points_xy -from compas.geometry import Line -from compas.geometry import offset_line -from compas.geometry import Frame -from compas.geometry import Scale -from compas.datastructures import Mesh -from compas.artists import MeshArtist -from compas.utilities import is_color_rgb -from compas.utilities.colors import is_color_light - -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class MeshArtist(PlotterArtist, MeshArtist): - """Artist for COMPAS mesh data structures. - - Parameters - ---------- - mesh : :class:`~compas.datastructures.Mesh` - A COMPAS mesh. - vertices : list[int], optional - Selection of vertex identifiers. - Default is None, in which case all vertices are drawn. - edges : list[tuple[int, int]], optional - Selection of edge identifiers. - The default is None, in which case all edges are drawn. - faces : list[int], optional - Selection of face identifiers. - The default is None, in which case all faces are drawn. - vertexcolor : tuple[float, float, float] | dict[int, tuple[float, float, float]], optional - Color specification for the vertices. - edgecolor : tuple[float, float, float] | dict[tuple[int, int], tuple[float, float, float]], optional - Color specification for the edges. - facecolor : tuple[float, float, float] | dict[int, tuple[float, float, float]], optional - Color specification for the faces. - show_vertices : bool, optional - If True, draw the vertices of the mesh. - show_edges : bool, optional - If True, draw the edges of the mesh. - show_faces : bool, optional - If True, draw the faces of the mesh. - vertexsize : int, optional - Size of the vertices. - vertextext : str | dict[int, str], optional - Labels for the vertices. - edgetext : str | dict[tuple[int, int], str], optional - Labels for the edges. - facetext : str | dict[int, str], optional - Labels for the faces. - sizepolicy : {'relative', 'absolute'}, optional - The policy for sizing the vertices. - If ``'relative'``, the value of `vertexsize` is scaled by the number of vertices. - If ``'absolute'``, the value of `vertexsize` is scaled by the resolution of the plotter (:attr:MeshArtist.plotter.dpi). - zorder : int, optional - The base stacking order of the components of the mesh on the canvas. - **kwargs : dict, optional - Additional keyword arguments. - See :class:`~compas_plotters.artists.PlotterArtist` and :class:`~compas.artists.MeshArtist` for more info. - - Attributes - ---------- - halfedges : list[tuple[int, int]] - The halfedges to include in the drawing. - vertex_size : dict[int, float] - Mapping between vertex identifiers and vertex sizes. - halfedge_color : dict[tuple[int, int], tuple[float, float, float]] - Mapping between halfedge identifiers and halfedge colors. - zorder_faces : int, read-only - The stacking order of the faces relative to the base stacking order of the mesh. - zorder_edges : int, read-only - The stacking order of the edges relative to the base stacking order of the mesh. - zorder_vertices : int, read-only - The stacking order of the vertices relative to the base stacking order of the mesh. - - """ - - default_halfedgecolor = (0.7, 0.7, 0.7) - - def __init__( - self, - mesh: Mesh, - vertices: Optional[List[int]] = None, - edges: Optional[List[int]] = None, - faces: Optional[List[int]] = None, - vertexcolor: Color = (1.0, 1.0, 1.0), - edgecolor: Color = (0.0, 0.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, - vertextext: Optional[Union[str, Dict[int, str]]] = None, - edgetext: Optional[Union[str, Dict[Tuple[int, int], str]]] = None, - facetext: Optional[Union[str, Dict[int, str]]] = None, - sizepolicy: Literal["relative", "absolute"] = "relative", - zorder: int = 1000, - **kwargs: Any, - ): - - super().__init__(mesh=mesh, **kwargs) - - self.sizepolicy = sizepolicy - self.vertices = vertices - self.edges = edges - self.faces = faces - self.vertex_color = vertexcolor - self.vertex_size = vertexsize - self.vertex_text = vertextext - self.edge_color = edgecolor - self.edge_width = edgewidth - self.face_color = facecolor - self.show_vertices = show_vertices - self.show_edges = show_edges - self.show_faces = show_faces - self.zorder = zorder - - self._halfedges = None - self._halfedgecollection = None - self._halfedge_color = None - - @property - def halfedges(self): - if not self._halfedges: - self._halfedges = [(u, v) for u in self.mesh.halfedge for v in self.mesh.halfedge[u]] - return self._halfedges - - @halfedges.setter - def halfedges(self, halfedges): - self._halfedges = halfedges - - @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 halfedge_color(self): - if self._halfedge_color is None: - self._halfedge_color = { - (u, v): self.default_halfedgecolor for u in self.mesh.halfedge for v in self.mesh.halfedge[u] - } - return self._halfedge_color - - @halfedge_color.setter - def halfedge_color(self, halfedge_color): - if isinstance(halfedge_color, dict): - self._halfedge_color = halfedge_color - elif is_color_rgb(halfedge_color): - self._halfedge_color = {(u, v): halfedge_color for u in self.mesh.halfedge for v in self.mesh.halfedge[u]} - - @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``""" - return self.mesh - - @item.setter - def item(self, item: Mesh): - self.mesh = item - - @property - def data(self) -> List[List[float]]: - return self.mesh.vertices_attributes("xy") - - # ============================================================================== - # clear and draw - # ============================================================================== - - def clear(self) -> None: - pass - - def clear_vertices(self) -> None: - """Clear the current vertices from the canvas. - - Returns - ------- - None - - """ - if self._vertexcollection: - self._vertexcollection.remove() - self._vertexcollection = None - - def clear_edges(self) -> None: - """Clear the current edges from the canvas. - - Returns - ------- - None - - """ - if self._edgecollection: - self._edgecollection.remove() - self._edgecollection = None - - def clear_halfedges(self) -> None: - """Clear the current halfedges from the canvas. - - Returns - ------- - None - - """ - if self._halfedgecollection: - for artist in self._halfedgecollection: - artist.remove() - self._halfedgecollection = None - - def clear_faces(self) -> None: - """Clear the current faces from the canvas. - - Returns - ------- - None - - """ - if self._facecollection: - self._facecollection.remove() - self._facecollection = None - - def draw( - self, - vertices: Optional[List[int]] = None, - edges: Optional[List[Tuple[int, int]]] = None, - faces: Optional[List[int]] = None, - vertexcolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, - edgecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, - facecolor: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, - ) -> None: - """Draw the mesh. - - Parameters - ---------- - vertices : list[int], optional - A list of vertex identifiers. - Default is None, in which case all vertices are drawn. - edges : list[tuple[int, int]], 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[int], optional - A list of face identifiers. - The default is None, in which case all faces are drawn. - vertexcolor : rgb-tuple | dict[int, rgb-tuple], optional - The color specification for the vertices. - edgecolor : rgb-tuple | dict[tuple[int, int], rgb-tuple], optional - The color specification for the edges. - facecolor : rgb-tuple | dict[int, rgb-tuple], optional - The color specification for the faces. - - Returns - ------- - None - - """ - 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) - - def draw_mesh(self): - raise NotImplementedError - - def draw_vertices( - self, - vertices: Optional[List[int]] = None, - color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, - ) -> None: - """Draw a selection of vertices. - - Parameters - ---------- - vertices : list[int], optional - A list of vertex identifiers. - Default is None, in which case all vertices are drawn. - color : rgb-tuple | dict[int, rgb-tuple], optional - The color specification for the vertices. - - Returns - ------- - None - - """ - self.clear_vertices() - - if vertices: - self.vertices = vertices - if color: - self.vertex_color = color - - circles = [] - for vertex in self.vertices: - x, y = self.vertex_xyz[vertex][:2] - circle = Circle( - [x, y], - radius=self.vertex_size.get(vertex, self.default_vertexsize), - facecolor=self.vertex_color.get(vertex, self.default_vertexcolor), - edgecolor=(0, 0, 0), - lw=0.3, - ) - circles.append(circle) - - collection = PatchCollection( - circles, - match_original=True, - zorder=self.zorder_vertices, - alpha=1.0, - picker=5, - ) - self.plotter.axes.add_collection(collection) - self._vertexcollection = collection - - def draw_edges( - self, - edges: Optional[List[Tuple[int, int]]] = None, - color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, - ) -> None: - """Draw a selection of edges. - - Parameters - ---------- - edges : list[tuple[int, int]], 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 | dict[tuple[int, int], rgb-tuple], optional - The color specification for the edges. - - Returns - ------- - None - - """ - self.clear_edges() - if edges: - self.edges = edges - if color: - self.edge_color = color - - lines = [] - colors = [] - widths = [] - for edge in self.edges: - u, v = edge - lines.append([self.vertex_xyz[edge[0]][:2], self.vertex_xyz[edge[1]][:2]]) - colors.append(self.edge_color.get(edge, self.edge_color.get((v, u), self.default_edgecolor))) - widths.append(self.edge_width.get(edge, self.edge_width.get((v, u), self.default_edgewidth))) - - collection = LineCollection( - lines, - linewidths=widths, - colors=colors, - linestyle="solid", - alpha=1.0, - zorder=self.zorder_edges, - ) - self.plotter.axes.add_collection(collection) - self._edgecollection = collection - - def draw_halfedges( - self, - halfedges: Optional[List[Tuple[int, int]]] = None, - color: Union[str, Color, List[Color], Dict[int, Color]] = (0.7, 0.7, 0.7), - distance: float = 0.05, - width: float = 0.01, - shrink: float = 0.8, - ) -> None: - """Draw a selection of halfedges. - - Parameters - ---------- - edges : list[tuple[int, int]], optional - A list of halfedges to draw. - The default is None, in which case all halfedges are drawn. - color : rgb-tuple | dict[tuple[int, int], rgb-tuple], optional - The color specification for the halfedges. - - Returns - ------- - None - - """ - self.clear_halfedges() - self._halfedgecollection = [] - - if color: - self.halfedge_color = color - - if halfedges: - self.halfedges = halfedges - - for u, v in self.halfedges: - face = self.mesh.halfedge_face(u, v) - - if face is None: - normal = self.mesh.face_normal(self.mesh.halfedge_face(v, u)) - else: - normal = self.mesh.face_normal(face) - - a, b = self.mesh.edge_coordinates(u, v) - line = Line(*offset_line((a, b), distance, normal)) - frame = Frame(line.midpoint, [1, 0, 0], [0, 1, 0]) - scale = Scale.from_factors([shrink, shrink, shrink], frame=frame) - line.transform(scale) - - artist = self.plotter.axes.arrow( - line.start[0], - line.start[1], - line.vector[0], - line.vector[1], - width=width, - head_width=10 * width, - head_length=10 * width, - length_includes_head=True, - shape="right", - color=self.halfedge_color.get((u, v), self.default_halfedgecolor), - zorder=10000, - ) - self._halfedgecollection.append(artist) - - def draw_faces( - self, - faces: Optional[List[int]] = None, - color: Optional[Union[str, Color, List[Color], Dict[int, Color]]] = None, - ) -> None: - """Draw a selection of faces. - - Parameters - ---------- - faces : list[int], optional - A list of face identifiers. - The default is None, in which case all faces are drawn. - color : rgb-tuple | dict[int, rgb-tuple], optional - The color specification for the faces. - - Returns - ------- - None - - """ - self.clear_faces() - if faces: - self.faces = faces - if color: - self.face_color = color - - polygons = [] - facecolors = [] - edgecolors = [] - linewidths = [] - for face in self.faces: - data = [self.vertex_xyz[vertex][:2] for vertex in self.mesh.face_vertices(face)] - polygons.append(PolygonPatch(data)) - facecolors.append(self.face_color.get(face, self.default_facecolor)) - edgecolors.append((0, 0, 0)) - linewidths.append(0.1) - - collection = PatchCollection( - polygons, - facecolors=facecolors, - edgecolors=edgecolors, - lw=linewidths, - alpha=1.0, - linestyle="solid", - zorder=self.zorder_faces, - ) - self.plotter.axes.add_collection(collection) - self._facecollection = collection - - def draw_vertexlabels(self, text: Optional[Dict[int, str]] = None) -> None: - """Draw a selection of vertex labels. - - Parameters - ---------- - text : dict[int, str], optional - A vertex-label map. - - Returns - ------- - None - - """ - if self._vertexlabelcollection: - for artist in self._vertexlabelcollection: - artist.remove() - - if text: - self.vertex_text = text - - labels = [] - for vertex in self.vertices: - bgcolor = self.vertex_color.get(vertex, self.default_vertexcolor) - color = (0, 0, 0) if is_color_light(bgcolor) else (1, 1, 1) - - text = self.vertex_text.get(vertex, None) - if text is None: - continue - - x, y = self.vertex_xyz[vertex][:2] - artist = self.plotter.axes.text( - x, - y, - f"{text}", - fontsize=self.plotter.fontsize, - family="monospace", - ha="center", - va="center", - zorder=10000, - color=color, - ) - labels.append(artist) - - self._vertexlabelcollection = labels - - def draw_edgelabels(self, text: Optional[Dict[int, str]] = None) -> None: - """Draw a selection of edge labels. - - Parameters - ---------- - text : dict[tuple[int, int], str] - An edge-label map. - - Returns - ------- - None - - """ - if self._edgelabelcollection: - for artist in self._edgelabelcollection: - artist.remove() - - if text: - self.edge_text = text - - labels = [] - for edge in self.edges: - u, v = edge - text = self.edge_text.get(edge, self.edge_text.get((v, u), None)) - if text is None: - continue - - x0, y0 = self.vertex_xyz[edge[0]][:2] - x1, y1 = self.vertex_xyz[edge[1]][:2] - x = 0.5 * (x0 + x1) - y = 0.5 * (y0 + y1) - - artist = self.plotter.axes.text( - x, - y, - f"{text}", - fontsize=self.plotter.fontsize, - family="monospace", - ha="center", - va="center", - zorder=10000, - color=(0, 0, 0), - bbox=dict( - boxstyle="round, pad=0.3", - facecolor=(1, 1, 1), - edgecolor=None, - linewidth=0, - ), - ) - labels.append(artist) - - self._edgelabelcollection = labels - - def draw_facelabels(self, text: Optional[Dict[int, str]] = None) -> None: - """Draw a selection of face labels. - - Parameters - ---------- - text : dict[int, str] - A face-label map. - - Returns - ------- - None - - """ - if self._facelabelcollection: - for artist in self._facelabelcollection: - artist.remove() - - if text: - self.face_text = text - - labels = [] - for face in self.faces: - text = self.face_text.get(face, None) - if text is None: - continue - - x, y, _ = centroid_points_xy([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)]) - - artist = self.plotter.axes.text( - x, - y, - f"{text}", - fontsize=self.plotter.fontsize, - family="monospace", - ha="center", - va="center", - zorder=10000, - color=(0, 0, 0), - bbox=dict( - boxstyle="circle, pad=0.5", - facecolor=(1, 1, 1), - edgecolor=(0.5, 0.5, 0.5), - linestyle=":", - ), - ) - labels.append(artist) - - self._facelabelcollection = labels - - def redraw(self) -> None: - """Redraw the mesh using the current geometry. - - Returns - ------- - None - - """ - pass - - def update_vertexcolors(self, colors): - """Update the colors of the vertices. - - Parameters - ---------- - colors : dict[int, tuple[float, float, float]] - Mapping between vertex identifiers and colors. - Missing vertices get the default color: :attr:`MeshArtist.default_vertexcolor`. - - Returns - ------- - None - - """ - facecolors = [] - for vertex in self.vertices: - if vertex in colors: - color = colors[vertex] - else: - color = self.vertex_color.get(vertex, self.default_vertexcolor) - facecolors.append(color) - self._vertexcollection.set_facecolors(facecolors) - - def update_edgecolors(self, colors): - """Update the colors of the edges. - - Parameters - ---------- - colors : dict[tuple[int, int], tuple[float, float, float]] - Mapping between edge identifiers and colors. - Missing edge get the default color: :attr:`MeshArtist.default_edgecolor`. - - Returns - ------- - None - - """ - edgecolors = [] - for edge in self.edges: - if edge in colors: - color = colors[edge] - else: - color = self.edge_color.get(edge, self.default_edgecolor) - edgecolors.append(color) - self._edgecollection.set_colors(edgecolors) - - def update_edgewidths(self, widths): - """Update the widths of the edges. - - Parameters - ---------- - widths : dict[tuple[int, int], float] - Mapping between edge identifiers and linewidths. - Missing edges get the default edge linewidth: :attr:`MeshArtist.default_edgewidth`. - - Returns - ------- - None - - """ - edgewidths = [] - for edge in self.edges: - if edge in widths: - w = widths[edge] - else: - w = self.edge_width.get(edge, self.default_edgewidth) - edgewidths.append(w) - self._edgecollection.set_linewidths(edgewidths) diff --git a/src/compas_plotters/artists/networkartist.py b/src/compas_plotters/artists/networkartist.py deleted file mode 100644 index a9a88ab66fc..00000000000 --- a/src/compas_plotters/artists/networkartist.py +++ /dev/null @@ -1,368 +0,0 @@ -from typing import Dict -from typing import Tuple -from typing import List -from typing import Union -from typing import Optional -from typing_extensions import Literal - -from matplotlib.collections import LineCollection, PatchCollection -from matplotlib.patches import Circle - -from compas.datastructures import Network -from compas.artists import NetworkArtist -from compas.utilities.colors import is_color_light -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class NetworkArtist(PlotterArtist, NetworkArtist): - """Artist for COMPAS network data structures. - - Parameters - ---------- - network : :class:`~compas.datastructures.Network` - A COMPAS network. - nodes : list[int], optional - Selection of node identifiers. - Default is None, in which case all nodes are drawn. - edges : list[tuple[int, int]], optional - Selection of edge identifiers. - The default is None, in which case all edges are drawn. - nodecolor : tuple[float, float, float] | dict[int, tuple[float, float, float]], optional - Color specification for the nodes. - edgecolor : tuple[float, float, float] | dict[tuple[int, int], tuple[float, float, float]], optional - Color specification for the edges. - show_nodes : bool, optional - If True, draw the nodes of the network. - show_edges : bool, optional - If True, draw the edges of the network. - nodesize : int, optional - The size of the nodes. - sizepolicy : {'relative', 'absolute'}, optional - The policy for sizing the nodes. - If ``'relative'``, the value of `nodesize` is scaled by the number of nodes. - If ``'absolute'``, the value of `nodesize` is scaled by the resolution of the plotter (:attr:NetworkArtist.plotter.dpi). - - Attributes - ---------- - node_size : dict[int, float] - Mapping between node identifiers and node sizes. - zorder_edges : int, read-only - The stacking order of the edges relative to the base stacking order of the network. - zorder_nodes : int, read-only - The stacking order of the nodes relative to the base stacking order of the network. - - """ - - def __init__( - self, - network: Network, - nodes: Optional[List[int]] = None, - edges: Optional[List[int]] = None, - nodecolor: Color = (1.0, 1.0, 1.0), - edgecolor: Color = (0.0, 0.0, 0.0), - edgewidth: float = 1.0, - show_nodes: bool = True, - show_edges: bool = True, - nodesize: int = 5, - sizepolicy: Literal["relative", "absolute"] = "relative", - zorder: int = 1000, - **kwargs, - ): - - super().__init__(network=network, **kwargs) - - self.sizepolicy = sizepolicy - 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.zorder = zorder - - @property - def zorder_edges(self): - return self.zorder + 10 - - @property - def zorder_nodes(self): - return self.zorder + 20 - - @property - def item(self): - """Network: Alias for ``~NetworkArtist.network``""" - return self.network - - @item.setter - def item(self, item: Network): - self.network = item - - @property - def data(self) -> List[List[float]]: - return self.network.nodes_attributes("xy") - - @property - def node_size(self): - if not self._node_size: - factor = self.plotter.dpi if self.sizepolicy == "absolute" else self.network.number_of_nodes() - size = self.default_nodesize / factor - self._node_size = {node: size for node in self.network.nodes()} - return self._node_size - - @node_size.setter - def node_size(self, nodesize): - factor = self.plotter.dpi if self.sizepolicy == "absolute" else self.network.number_of_nodes() - if isinstance(nodesize, dict): - self.node_size.update({node: size / factor for node, size in nodesize.items()}) - elif isinstance(nodesize, (int, float)): - self._node_size = {node: nodesize / factor for node in self.network.nodes()} - - # ============================================================================== - # clear and draw - # ============================================================================== - - def clear_nodes(self): - """Clear the current nodes from the canvas. - - Returns - ------- - None - - """ - if self._nodecollection: - self._nodecollection.remove() - self._nodecollection = None - - def clear_edges(self): - """Clear the current edges from the canvas. - - Returns - ------- - None - - """ - if self._edgecollection: - self._edgecollection.remove() - 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[int], optional - A list of node identifiers. - Default is None, in which case all nodes are drawn. - edges : list[tuple[int, int]], 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 : tuple[float, float, float] | dict[int, tuple[float, float, float]], optional - The color specification for the nodes. - edgecolor : tuple[float, float, float] | dict[tuple[int, int], tuple[float, float, float]], optional - The color specification for the edges. - - Returns - ------- - None - - """ - self.clear() - 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, - ) -> None: - """Draw a selection of nodes. - - Parameters - ---------- - nodes : list[int], optional - A list of node identifiers. - Default is None, in which case all nodes are drawn. - color : tuple[float, float, float] | dict[int, tuple[float, float, float]], optional - The color specification for the nodes. - - Returns - ------- - None - - """ - self.clear_nodes() - if nodes: - self.nodes = nodes - if color: - self.node_color = color - - circles = [] - for node in self.nodes: - x, y = self.node_xyz[node][:2] - circle = Circle( - [x, y], - radius=self.node_size.get(node, self.default_nodesize), - facecolor=self.node_color.get(node, self.default_nodecolor), - edgecolor=(0, 0, 0), - lw=0.3, - ) - 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[tuple[int, int]], 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 : tuple[float, float, float] | dict[tuple[int, int], tuple[float, float, float]], optional - The color specification for the edges. - - Returns - ------- - None - - """ - self.clear_edges() - if edges: - self.edges = edges - if color: - self.edge_color = color - - lines = [] - colors = [] - widths = [] - for edge in self.edges: - lines.append([self.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 - - def draw_nodelabels(self, text: Optional[Dict[int, str]] = None) -> None: - """Draw a selection of node labels. - - Parameters - ---------- - text : dict[int, str], optional - A node-label map. - - Returns - ------- - None - - """ - if self._nodelabelcollection: - for artist in self._nodelabelcollection: - artist.remove() - - if text: - self.node_text = text - - labels = [] - for node in self.nodes: - bgcolor = self.node_color.get(node, self.default_nodecolor) - color = (0, 0, 0) if is_color_light(bgcolor) else (1, 1, 1) - - text = self.node_text.get(node, None) - if text is None: - continue - - x, y = self.node_xyz[node][:2] - artist = self.plotter.axes.text( - x, - y, - f"{text}", - fontsize=self.plotter.fontsize, - family="monospace", - ha="center", - va="center", - zorder=10000, - color=color, - ) - labels.append(artist) - - self._nodelabelcollection = labels - - def draw_edgelabels(self, text: Optional[Dict[int, str]] = None) -> None: - """Draw a selection of edge labels. - - Parameters - ---------- - text : dict[tuple[int, int], str] - An edge-label map. - - Returns - ------- - None - - """ - if self._edgelabelcollection: - for artist in self._edgelabelcollection: - artist.remove() - - if text: - self.edge_text = text - - labels = [] - for edge in self.edges: - u, v = edge - text = self.edge_text.get(edge, self.edge_text.get((v, u), None)) - if text is None: - continue - - x0, y0 = self.node_xyz[edge[0]][:2] - x1, y1 = self.node_xyz[edge[1]][:2] - x = 0.5 * (x0 + x1) - y = 0.5 * (y0 + y1) - - artist = self.plotter.axes.text( - x, - y, - f"{text}", - fontsize=self.plotter.fontsize, - family="monospace", - ha="center", - va="center", - zorder=10000, - color=(0, 0, 0), - bbox=dict( - boxstyle="round, pad=0.3", - facecolor=(1, 1, 1), - edgecolor=None, - linewidth=0, - ), - ) - labels.append(artist) - - self._edgelabelcollection = labels diff --git a/src/compas_plotters/artists/pointartist.py b/src/compas_plotters/artists/pointartist.py deleted file mode 100644 index 30ec854e1f6..00000000000 --- a/src/compas_plotters/artists/pointartist.py +++ /dev/null @@ -1,120 +0,0 @@ -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.artists import PrimitiveArtist -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class PointArtist(PlotterArtist, PrimitiveArtist): - """Artist for COMPAS points. - - Parameters - ---------- - point : :class:`~compas.geometry.Point` - A COMPAS point. - size : int, optional - The size of the point. - facecolor : Color, optional - Color of the interior of the point representing the point. - edgecolor : Color, optional - Color of the boundary of the point representing the point. - zorder : int, optional - Stacking order above the XY plane of the plotter canvas. - **kwargs : dict, optional - Additional keyword arguments. See :class:`PlotterArtist` or :class:`PrimitiveArtist`. - - Attributes - ---------- - point : :class:`~compas.geometry.Point` - The point associated with the artist. - size : float - Size of the point, relative to the resolution of the plotter. - ``size = self._size / self.plotter.dpi``. - - """ - - 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) - - self._mpl_circle = None - self._size = None - self.size = size - self.facecolor = facecolor - self.edgecolor = edgecolor - self.zorder = zorder - - @property - def point(self) -> Point: - return self.primitive - - @point.setter - def point(self, point: Point): - self.primitive = point - - @property - def _T(self): - F = self.plotter.figure.dpi_scale_trans - S = ScaledTranslation(self.point[0], self.point[1], self.plotter.axes.transData) - T = F + S - return T - - @property - def size(self) -> float: - return self._size / self.plotter.dpi - - @size.setter - def size(self, size: int): - self._size = size - - @property - def data(self) -> List[List[float]]: - return [self.point[:2]] - - def draw(self) -> None: - """Draw the circle. - - Returns - ------- - None - - """ - circle = Circle( - [0, 0], - radius=self.size, - facecolor=self.facecolor, - edgecolor=self.edgecolor, - transform=self._T, - zorder=self.zorder, - ) - self._mpl_circle = self.plotter.axes.add_artist(circle) - self.update_data() - - def redraw(self) -> None: - """Update the point using the current geometry and visualization settings. - - Returns - ------- - None - - """ - self._mpl_circle.set_radius(self.size) - self._mpl_circle.set_edgecolor(self.edgecolor) - self._mpl_circle.set_facecolor(self.facecolor) - self._mpl_circle.set_transform(self._T) - self.update_data() diff --git a/src/compas_plotters/artists/polygonartist.py b/src/compas_plotters/artists/polygonartist.py deleted file mode 100644 index f65ac5f7d6e..00000000000 --- a/src/compas_plotters/artists/polygonartist.py +++ /dev/null @@ -1,110 +0,0 @@ -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.artists import PrimitiveArtist -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class PolygonArtist(PlotterArtist, PrimitiveArtist): - """Artist for COMPAS polygons. - - Parameters - ---------- - polygon : :class:`~compas.geometry.Polygon` - A COMPAS polygon. - linewidth : float, optional - Width of the polygon edge lines. - linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional - Style of the line. - facecolor : tuple[float, float, float], optional - Color of the interior face of the polygon. - edgecolor : tuple[float, float, float], optional - Color of the boundary of the polygon. - zorder : int, optional - Stacking order of the polygon on the canvas. - **kwargs : dict, optional - Additional keyword arguments. - See :class:`~compas_plotters.artists.PlotterArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. - - Attributes - ---------- - polygon : :class:`~compas.geometry.Polygon` - The polygon associated with the artist. - - """ - - def __init__( - self, - polygon: Polygon, - linewidth: float = 1.0, - linestyle: Literal["solid", "dotted", "dashed", "dashdot"] = "solid", - facecolor: Color = (1.0, 1.0, 1.0), - edgecolor: Color = (0, 0, 0), - fill: bool = True, - alpha: float = 1.0, - zorder: int = 1000, - **kwargs: Any - ): - - super().__init__(primitive=polygon, **kwargs) - - self._mpl_polygon = None - self.linewidth = linewidth - self.linestyle = linestyle - self.facecolor = facecolor - self.edgecolor = edgecolor - self.fill = fill - self.alpha = alpha - self.zorder = zorder - - @property - def polygon(self): - return self.primitive - - @polygon.setter - def polygon(self, polygon): - self.primitive = polygon - - @property - def data(self) -> List[List[float]]: - return [point[:2] for point in self.polygon.points] - - def draw(self) -> None: - """Draw the polygon. - - Returns - ------- - None - - """ - polygon = PolygonPatch( - self.data, - linewidth=self.linewidth, - linestyle=self.linestyle, - facecolor=self.facecolor, - edgecolor=self.edgecolor, - zorder=self.zorder, - alpha=self.alpha, - fill=self.fill, - ) - self._mpl_polygon = self.plotter.axes.add_patch(polygon) - - def redraw(self) -> None: - """Update the polygon using the current geometry and visualization settings. - - Returns - ------- - None - - """ - self._mpl_polygon.set_xy(self.data) - self._mpl_polygon.set_facecolor(self.facecolor) - self._mpl_polygon.set_edgecolor(self.edgecolor) - self._mpl_polygon.set_linewidth(self.linewidth) diff --git a/src/compas_plotters/artists/polylineartist.py b/src/compas_plotters/artists/polylineartist.py deleted file mode 100644 index 779c9513df8..00000000000 --- a/src/compas_plotters/artists/polylineartist.py +++ /dev/null @@ -1,110 +0,0 @@ -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.artists import PrimitiveArtist -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class PolylineArtist(PlotterArtist, PrimitiveArtist): - """Artist for COMPAS polylines. - - Parameters - ---------- - polyline : :class:`~compas.geometry.Polyline` - A COMPAS polyline. - linewidth : float, optional - Width of the polyline edge lines. - linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional - Style of the line. - facecolor : tuple[float, float, float], optional - Color of the interior face of the polyline. - edgecolor : tuple[float, float, float], optional - Color of the boundary of the polyline. - zorder : int, optional - Stacking order of the polyline on the canvas. - **kwargs : dict, optional - Additional keyword arguments. - See :class:`~compas_plotters.artists.PlotterArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. - - Attributes - ---------- - polyline : :class:`~compas.geometry.Polyline` - The line associated with the artist. - - """ - - 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) - - self._mpl_line = None - self._point_artists = [] - self.draw_points = draw_points - self.linewidth = linewidth - self.linestyle = linestyle - self.color = color - self.zorder = zorder - - @property - def polyline(self): - return self.primitive - - @polyline.setter - def polyline(self, polyline): - self.primitive = polyline - - @property - def data(self) -> List[List[float]]: - return [point[:2] for point in self.polyline.points] - - def draw(self) -> None: - """Draw the polyline. - - Returns - ------- - None - - """ - x, y, _ = zip(*self.polyline.points) - line2d = Line2D( - x, - y, - linewidth=self.linewidth, - linestyle=self.linestyle, - color=self.color, - zorder=self.zorder, - ) - self._mpl_line = self.plotter.axes.add_line(line2d) - if self.draw_points: - for point in self.polyline: - self._point_artists.append(self.plotter.add(point)) - - def redraw(self) -> None: - """Update the polyline using the current geometry and visualization settings. - - Returns - ------- - None - - """ - x, y, _ = zip(*self.polyline.points) - self._mpl_line.set_xdata(x) - self._mpl_line.set_ydata(y) - self._mpl_line.set_color(self.color) - self._mpl_line.set_linewidth(self.linewidth) diff --git a/src/compas_plotters/artists/segmentartist.py b/src/compas_plotters/artists/segmentartist.py deleted file mode 100644 index adb2cdf903a..00000000000 --- a/src/compas_plotters/artists/segmentartist.py +++ /dev/null @@ -1,112 +0,0 @@ -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.artists import PrimitiveArtist -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class SegmentArtist(PlotterArtist, PrimitiveArtist): - """Artist for drawing COMPAS lines as segments. - - Parameters - ---------- - line : :class:`~compas.geometry.Line` - A COMPAS line. - draw_points : bool, optional - If True, draw the start and end point of the line. - linewidth : float, optional - Width of the line. - linestyle : {'solid', 'dotted', 'dashed', 'dashdot'}, optional - Style of the line. - color : tuple[float, float, float], optional - Color of the line. - zorder : int, optional - Stacking order of the line on the canvas. - **kwargs : dict, optional - Additional keyword arguments. - See :class:`~compas_plotters.artists.PlotterArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. - - Attributes - ---------- - line : :class:`~compas.geometry.Line` - The line associated with the artist. - - """ - - 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) - - self._mpl_line = None - self._start_artist = None - self._end_artist = None - self.draw_points = draw_points - self.linestyle = linestyle - self.linewidth = linewidth - self.color = color - self.zorder = zorder - - @property - def line(self): - return self.primitive - - @line.setter - def line(self, line): - self.primitive = line - - @property - def data(self) -> List[List[float]]: - return [self.line.start[:2], self.line.end[:2]] - - def draw(self) -> None: - """Draw the line associated with the artist. - - Returns - ------- - None - - """ - line2d = Line2D( - [self.line.start[0], self.line.end[0]], - [self.line.start[1], self.line.end[1]], - linewidth=self.linewidth, - linestyle=self.linestyle, - color=self.color, - zorder=self.zorder, - ) - self._mpl_line = self.plotter.axes.add_line(line2d) - if self.draw_points: - self._start_artist = self.plotter.add(self.line.start, edgecolor=self.color) - self._end_artist = self.plotter.add(self.line.end, edgecolor=self.color) - - def redraw(self) -> None: - """Update the line using the current geometry and visualization settings. - - Returns - ------- - None - - """ - self._mpl_line.set_xdata([self.line.start[0], self.line.end[0]]) - self._mpl_line.set_ydata([self.line.start[1], self.line.end[1]]) - self._mpl_line.set_color(self.color) - self._mpl_line.set_linewidth(self.linewidth) - if self.draw_points: - self._start_artist.redraw() - self._end_artist.redraw() diff --git a/src/compas_plotters/artists/vectorartist.py b/src/compas_plotters/artists/vectorartist.py deleted file mode 100644 index 8e5f5d73680..00000000000 --- a/src/compas_plotters/artists/vectorartist.py +++ /dev/null @@ -1,106 +0,0 @@ -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.artists import PrimitiveArtist -from .artist import PlotterArtist - -Color = Tuple[float, float, float] - - -class VectorArtist(PlotterArtist, PrimitiveArtist): - """Artist for COMPAS vectors. - - Parameters - ---------- - vector : :class:`~compas.geometry.Vector` - A COMPAS vector. - point : :class:`~compas.geometry.Point`, optional - A COMPAS point as base point for the vector. - Default is the origin of the world coordinate system. - draw_point : bool, optional - If True, draw the point of application of the vector. - color : tuple[float, float, float], optional - Color of the vector. - zorder : int, optional - Stacking order of the vector on the canvas. - **kwargs : dict, optional - Additional keyword arguments. - See :class:`~compas_plotters.artists.PlotterArtist` and :class:`~compas.artists.PrimitiveArtist` for more info. - - Attributes - ---------- - vector : :class:`~compas.geometry.Vector` - The vector associated with the artist. - - """ - - 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) - - 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.color = color - self.zorder = zorder - - @property - def vector(self): - return self.primitive - - @vector.setter - def vector(self, vector): - self.primitive = vector - - @property - def data(self) -> List[List[float]]: - return [self.point[:2], (self.point + self.vector)[:2]] - - def draw(self) -> None: - """Draw the vector. - - Returns - ------- - None - - """ - 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, - facecolor=self.color, - zorder=self.zorder, - mutation_scale=100, - ) - if self.draw_point: - self._point_artist = self.plotter.add(self.point, edgecolor=self.color) - self._mpl_vector = self.plotter.axes.add_patch(arrow) - - def redraw(self): - """Update the vector using the current geometry and visualization settings. - - Returns - ------- - None - - """ - self._mpl_vector.set_positions(self.point[:2], (self.point + self.vector)[:2]) - if self.draw_point: - self._point_artist.redraw() diff --git a/src/compas_plotters/core/__init__.py b/src/compas_plotters/core/__init__.py deleted file mode 100644 index 5c599360b17..00000000000 --- a/src/compas_plotters/core/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -from .utilities import ( - get_axes_dimension, - assert_axes_dimension, - width_to_dict, - size_to_sizedict, -) -from .helpers import Axes2D, Axes3D, Bounds, Box, Cloud2D, Cloud3D, Hull -from .drawing import ( - create_axes_xy, - create_axes_3d, - draw_points_xy, - draw_xpoints_xy, - draw_points_3d, - draw_lines_xy, - draw_xlines_xy, - draw_lines_3d, - draw_xarrows_xy, - draw_xlabels_xy, - draw_xpolygons_xy, - draw_xpolylines_xy, -) - -__all__ = [ - "get_axes_dimension", - "assert_axes_dimension", - "width_to_dict", - "size_to_sizedict", - "Axes2D", - "Axes3D", - "Bounds", - "Box", - "Cloud2D", - "Cloud3D", - "Hull", - "create_axes_xy", - "create_axes_3d", - "draw_points_xy", - "draw_xpoints_xy", - "draw_points_3d", - "draw_lines_xy", - "draw_xlines_xy", - "draw_lines_3d", - "draw_xarrows_xy", - "draw_xlabels_xy", - "draw_xpolygons_xy", - "draw_xpolylines_xy", -] diff --git a/src/compas_plotters/core/drawing.py b/src/compas_plotters/core/drawing.py deleted file mode 100644 index ee20b494a87..00000000000 --- a/src/compas_plotters/core/drawing.py +++ /dev/null @@ -1,774 +0,0 @@ -from numpy import asarray - -import matplotlib.pyplot as plt - -from matplotlib.patches import Circle -from matplotlib.patches import Polygon - -from matplotlib.collections import LineCollection -from matplotlib.collections import PatchCollection -from matplotlib.collections import PolyCollection - -from mpl_toolkits.mplot3d.art3d import Line3DCollection - -from compas.geometry import centroid_points_xy -from compas.geometry import midpoint_line_xy -from compas.utilities import color_to_rgb - - -ZORDER_POLYGONS = 1000 -ZORDER_LINES = 2000 -ZORDER_POINTS = 3000 -ZORDER_LABELS = 4000 - - -# ============================================================================== -# axes -# ============================================================================== - - -def create_axes_xy( - figsize=(8.0, 6.0), - dpi=100, - xlabel=None, - ylabel=None, - fontname="Times New Roman", - fontsize=10, - grid=False, - xlim=None, - ylim=None, - ticklength=20, - tickfontsize=10, - xscale="linear", - yscale="linear", - bgcolor="#ffffff", -): - """Initializes plot axes object for matplotlib plotting. - - Parameters - ---------- - figsize : 2-tuple of float, optional - Size of the figure. - Default is ``(8.0, 6.0)`` - dpi : int, optional - Resolution of the plot. - Default is ``100``. - xlabel : str, optional - Label for the x-axis. - Default is None. - ylabel : str, optional - Label for the y-axis. - Default is None. - fontname : str, optional - Fontname of the main labels and text. - Default is ``'Times New Roman'``. - fontsize : int, optional - Fontsize of the main labels and text. - Default is ``10``. - grid : bool, optional - Display grid. - Default is False. - xlim : 2-tuple, optional - Limits of the X-axis. - Default is None. - ylim : 2-tuple, optional - Limits of the Y-axis. - Default is None. - ticklength : float, optional - Length of the ticks. - Default is ``20``. - tickfontsize : int, optional - Fontsize of the ticks. - Default is ``10``. - xscale : {'linear', 'log'} - Scale of the X axis. - yscale : {'linear', 'log'} - Scale of the Y axis. - bgcolor : str | list, optional - Background color as hex string or rgb tuple. - Default is white. - - Returns - ------- - object - Matplotlib axes. - - """ - # mpl.rcParams['figure.figsize'] = figsize - # mpl.rcParams['figure.dpi'] = dpi - # mpl.rcParams['savefig.dpi'] = dpi - fig = plt.figure(facecolor=bgcolor, figsize=figsize, dpi=dpi) - axes = fig.add_subplot(111, aspect="equal") - axes.grid(b=grid) - axes.set_frame_on(False) - if xlabel: - axes.set_xlabel(xlabel, fontname=fontname, fontsize=fontsize) - if ylabel: - axes.set_ylabel(ylabel, fontname=fontname, fontsize=fontsize) - if xlim: - axes.set_xlim(xlim[0], xlim[1]) - if ylim: - axes.set_ylim(ylim[0], ylim[1]) - # plt.minorticks_on() - # plt.tick_params(which='major', length=ticklength, labelsize=tickfontsize) - # plt.tick_params(which='minor', length=ticklength * 0.33) - axes.set_xscale(xscale) - axes.set_yscale(yscale) - axes.set_xticks([]) - axes.set_yticks([]) - # axes.set_xmargin(0.05) - # axes.set_ymargin(0.05) - # axes.autoscale() - return axes - - -def create_axes_3d( - size=(10, 7), - xlabel="$x$", - ylabel="$y$", - zlabel="$z$", - fontname="Times New Roman", - fontsize=20, - grid=True, - limits=None, - ticklength=20, - tickfontsize=15, - angle=(30, 45), -): - """Initializes plot axes object for matplotlib plotting. - - Parameters - ---------- - size : 2-tuple of float, optinoal - Size of the figure. - Default is ``(10.0, 7.0)``. - xlabel : str, optional - Label for the x-axis. - Default is ``'$x$'``. - ylabel : str, optional - Label for the y-axis. - Default is ``'$y$'``. - zlabel : str, optional - Label for the z-axis. - Default is ``'$z$'``. - fontname : str, optional - Fontname of the main labels and text. - Default is ``'Times New Roman'``. - fontsize : int, optional - Fontsize of the main labels and text. - Default is ``10``. - grid : bool, optional - Display grid. - Default is False. - limits : dict, optional - Axis limits and tick spacing. - Default is None. - ticklength : float, optional - Length of the ticks. - Default is ``20``. - tickfontsize : int, optional - Fontsize of the ticks. - Default is ``15``. - angle : 2-tuple of float - Elevation and azimuth angles for 3D plots. - Default is ``'30.0, 45.0'``. - - Returns - ------- - object - Matplotlib axes. - - """ - fig = plt.figure(facecolor="white", figsize=size) - axes = fig.add_subplot(111, projection="3d", aspect="equal") - # axes.w_xaxis.set_pane_color((1, 1, 1, 1)) - # axes.w_yaxis.set_pane_color((1, 1, 1, 1)) - # axes.w_zaxis.set_pane_color((1, 1, 1, 1)) - axes.grid(b=grid) - axes.set_xlabel(xlabel, fontname=fontname, fontsize=fontsize) - axes.set_ylabel(ylabel, fontname=fontname, fontsize=fontsize) - axes.set_zlabel(zlabel, fontname=fontname, fontsize=fontsize) - # axes.view_init(elev=angle[0], azim=angle[1]) - axes.set_xticks([]) - axes.set_yticks([]) - axes.set_zticks([]) - axes.set_xmargin(0.05) - axes.set_ymargin(0.05) - axes.autoscale() - return axes - - -# ============================================================================== -# points -# ============================================================================== - - -def draw_points_xy(points, axes, facecolor="#ffffff", edgecolor="#000000", linewidth=0.5, radius=1.0): - """Creates an XY point collection and adds it to the axis. - - Parameters - ---------- - points : list - XY(Z) coordinates of the points. - axes : object - Matplotlib axes. - facecolor : str | list, optional - Color of the point face. - Default is white. - edgecolor : str | list, optional - Color of the point edge. - Default is black. - linewidth : float or list, optional - Width of the point edge. - Default is ``0.5``. - radius : float or list, optional - The radius of the points. - Default is ``1.0``. - - Returns - ------- - object - A collection of points. - - """ - p = len(points) - # preprocess patch parameters - if isinstance(facecolor, str): - facecolor = [facecolor] * p - if isinstance(edgecolor, str): - edgecolor = [edgecolor] * p - if isinstance(linewidth, (int, float)): - linewidth = float(linewidth) - linewidth = [linewidth] * p - if isinstance(radius, (int, float)): - radius = float(radius) - radius = [radius] * p - # -------------------------------------------------------------------------- - circles = [] - for i in range(p): - point = points[i] - circle = Circle(point[0:2], radius=radius[i]) - circles.append(circle) - coll = PatchCollection( - circles, - facecolors=facecolor, - edgecolors=edgecolor, - linewidhts=linewidth, - alpha=1.0, - zorder=ZORDER_POINTS, - ) - axes.add_collection(coll) - return coll - - -def draw_xpoints_xy(points, axes): - """Creates an XY point collection and adds it to the axis. - - 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. - - axes : object - Matplotlib axes. - - Returns - ------- - object - The matplotlib point collection object. - - """ - circles = [] - facecolors = [] - edgecolors = [] - linewidths = [] - for point in points: - pos = point["pos"] - radius = point["radius"] - text = point.get("text") - fcolor = point.get("facecolor") or "#ffffff" - ecolor = point.get("edgecolor") or "#000000" - lwidth = point.get("edgewidth") or 1.0 - textcolor = point.get("textcolor") or "#000000" - fontsize = point.get("fontsize") or 12 - circles.append(Circle(pos[0:2], radius=radius)) - facecolors.append(color_to_rgb(fcolor, normalize=True)) - edgecolors.append(color_to_rgb(ecolor, normalize=True)) - linewidths.append(lwidth) - if text is not None: - axes.text( - pos[0] - 0.01, - pos[1] - 0.01, - text, - fontsize=fontsize, - zorder=ZORDER_LABELS, - ha="center", - va="center", - color=textcolor, - ) - coll = PatchCollection( - circles, - linewidths=linewidths, - facecolors=facecolors, - edgecolors=edgecolors, - alpha=1.0, - zorder=ZORDER_POINTS, - ) - axes.add_collection(coll) - return coll - - -def draw_points_3d(points, axes, facecolor="#ffffff", edgecolor="#000000"): - """Creates a 3D point collection and adds it to the axis. - - Parameters - ---------- - points : list - XYZ coordinates of the points. - axes : object - Matplotlib axes. - facecolor : str | list, optional - Color of the face of the points. - Default is white. - edgecolor : str | list, optional - Color of the edge of the points. - Default is black. - - Returns - ------- - object - The matplotlib point collection object. - - """ - p = len(points) - points = asarray(points) - if isinstance(facecolor, str): - facecolor = [facecolor] * p - if isinstance(edgecolor, str): - edgecolor = [edgecolor] * p - x = points[:, 0] - y = points[:, 1] - z = points[:, 2] - coll, _ = axes.plot(x, y, z, "o", color=(1.0, 1.0, 1.0)) - return coll - - -# ============================================================================== -# lines -# ============================================================================== - - -def draw_lines_xy(lines, axes, linewidth=1.0, linestyle="-", color="#000000", alpha=1.0): - """Creates an XY line collection and adds it to the axis. - - Parameters - ---------- - lines : list - List of ((X1, Y1), (X2, X2)) lines. - axes : object - Matplotlib axes. - linewidth : float or list of float, optional - Width of the lines. - Default is ``1.0``. - linestyle : str | list of str, optional - Matplotlib line style strings. - Default is ``'-'``. - color : str | list of str, optional - Color of the lines. - Default is black. - alpha : float or list of float, optional - Opacity of the lines. - Default is ``1.0``. - - Returns - ------- - object - The matplotlib point collection object. - - """ - n = len(lines) - if isinstance(linewidth, (int, float)): - linewidth = float(linewidth) - linewidth = [linewidth] * n - if isinstance(color, str): - color = [color] * n - # -------------------------------------------------------------------------- - coll = LineCollection( - [(start[0:2], end[0:2]) for start, end in lines], - linewidths=linewidth, - colors=color, - linestyle=linestyle, - alpha=alpha, - zorder=ZORDER_LINES, - ) - axes.add_collection(coll) - return coll - - -def draw_xlines_xy(lines, axes, alpha=1.0, linestyle="solid"): - """Creates an XY line collection and adds it to the axis. - - Parameters - ---------- - lines : list - 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``. - - axes : object - Matplotlib axes. - alpha : float, optional - Opacity of the lines. - Default is ``1.0``. - linestyle : str, optional - Matplotlib line style strings. - Default is ``'solid'``. - - Returns - ------- - object - The matplotlib line collection object. - - """ - fromto = [] - widths = [] - colors = [] - for line in lines: - sp = line["start"] - ep = line["end"] - width = line.get("width", 1.0) - color = line.get("color", "#000000") - text = line.get("text", None) - textcolor = line.get("textcolor") or "#000000" - fontsize = line.get("fontsize") or 6 - fromto.append((sp[0:2], ep[0:2])) - widths.append(width) - colors.append(color_to_rgb(color, normalize=True)) - if text: - x, y, z = midpoint_line_xy((sp, ep)) - t = axes.text( - x, - y, - text, - fontsize=fontsize, - zorder=ZORDER_LABELS, - ha="center", - va="center", - color=color_to_rgb(textcolor, normalize=True), - ) - t.set_bbox({"color": "#ffffff", "alpha": 1.0}) - coll = LineCollection( - fromto, - linewidths=widths, - colors=colors, - linestyle=linestyle, - alpha=alpha, - zorder=ZORDER_LINES, - ) - axes.add_collection(coll) - return coll - - -def draw_lines_3d(lines, axes, linewidth=1.0, linestyle="solid", color="#000000"): - """Creates an 3D line collection and adds it to the axis. - - Parameters - ---------- - lines : list - Pairs of XYZ coordinates defining start and end points of the lines. - axes : object - Matplotlib axes. - linewidth : float or list of float, optional - Width for the lines. - Default is ``1.0``. - linestyle : str, optional - Matplotlib line style strings. - Default is ``'solid'``. - color : str | list of str, optional - Color of the lines. - Default is black. - - Returns - ------- - object - The matplotlib line collection object. - - """ - n = len(lines) - if isinstance(linewidth, (int, float)): - linewidth = float(linewidth) - linewidth = [linewidth] * n - if isinstance(color, str): - color = [color] * n - - coll = Line3DCollection( - lines, - linewidths=linewidth, - colors=color, - linestyle=linestyle, - zorder=ZORDER_LINES, - ) - axes.add_collection(coll) - return coll - - -# ============================================================================== -# polylines -# ============================================================================== - - -def draw_xpolylines_xy(polylines, axes): - paths = [] - - widths = [] - colors = [] - - for polyline in polylines: - points = polyline["points"] - width = polyline.get("width", 1.0) - color = polyline.get("color", "#000000") - text = polyline.get("text", None) - textcolor = polyline.get("textcolor") or "#000000" - fontsize = polyline.get("fontsize") or 6 - - path = [point[0:2] for point in points] - paths.append(path) - - widths.append(width) - colors.append(color_to_rgb(color, normalize=True)) - - if text: - p = len(points) - - if p % 2 == 0: - a = points[p // 2] - b = points[p // 2 + 1] - x, y, z = midpoint_line_xy((a, b)) - else: - x, y = points[p // 2 + 1] - - t = axes.text( - x, - y, - text, - fontsize=fontsize, - zorder=ZORDER_LABELS, - ha="center", - va="center", - color=color_to_rgb(textcolor, normalize=True), - ) - - t.set_bbox({"color": "#ffffff", "alpha": 1.0, "edgecolor": "#ffffff"}) - - coll = PolyCollection( - paths, - closed=False, - linewidths=widths, - edgecolors=colors, - facecolors="none", - zorder=ZORDER_LINES, - ) - - axes.add_collection(coll) - - return coll - - -# ============================================================================== -# arrows -# ============================================================================== - - -def draw_xarrows_xy(lines, axes): - """Creates an XY arrow collection and adds it to the axis. - - Parameters - ---------- - lines : list of dict - List of dictionaries containing the arrow line 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``. - - axes : object - Matplotlib axes. - - """ - arrowprops = { - "arrowstyle": "-|>,head_length=0.6,head_width=0.2", - "connectionstyle": "arc3,rad=0.0", - "linewidth": 1.0, - "color": "#000000", - "shrinkB": 0.0, - "shrinkA": 0.0, - } - xys = [] - for line in lines: - sp = line["start"][:2] - ep = line["end"][:2] - text = line.get("text", None) - textcolor = line.get("textcolor") or "#000000" - fontsize = line.get("fontsize") or 6 - arrowprops["color"] = color_to_rgb(line.get("color", "#000000"), normalize=True) - arrowprops["linewidth"] = line.get("width", 1.0) - axes.annotate( - "", - xy=ep, - xytext=sp, - arrowprops=arrowprops, - zorder=ZORDER_LINES, - ) - xys.append(sp) - xys.append(ep) - if text: - x, y, z = midpoint_line_xy((sp, ep)) - t = axes.text( - x, - y, - text, - fontsize=fontsize, - zorder=ZORDER_LABELS, - ha="center", - va="center", - color=color_to_rgb(textcolor, normalize=True), - ) - t.set_bbox({"color": "#ffffff", "alpha": 1.0, "edgecolor": "#ffffff"}) - axes.update_datalim(xys) - - -# ============================================================================== -# labels -# ============================================================================== - - -def draw_xlabels_xy(labels, axes): - """Creates a label collection and adds it to the axis. - - Parameters - ---------- - labels : list of dict - List of dictionaries containing the label properties. - axes : object - Matplotlib axes. - - """ - for label in labels: - x, y = label["pos"] - text = label["text"] - fontsize = label["fontsize"] - color = label.get("color") or "#ffffff" - textcolor = label.get("textcolor") or "#000000" - bbox = dict( - color=color_to_rgb(color, normalize=True), - edgecolor=color_to_rgb(color, normalize=True), - alpha=1.0, - pad=0.0, - ) - t = axes.text( - x, - y, - text, - fontsize=fontsize, - zorder=ZORDER_LABELS, - ha="center", - va="center", - color=color_to_rgb(textcolor, normalize=True), - ) - t.set_bbox(bbox) - - -# ============================================================================== -# faces -# ============================================================================== - - -def draw_xpolygons_xy(polygons, axes): - """Creates a polygon collection and adds it to the axis. - - 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``. - - axes : object - Matplotlib axes. - - Returns - ------- - object - The matplotlib polygon collection object. - - """ - facecolors = [] - edgecolors = [] - linewidths = [] - patches = [] - - for attr in polygons: - points = attr["points"] - text = attr.get("text") - textcolor = color_to_rgb(attr.get("textcolor", "#000000"), normalize=True) - - facecolors.append(color_to_rgb(attr.get("facecolor", "#ffffff"), normalize=True)) - edgecolors.append(color_to_rgb(attr.get("edgecolor", "#000000"), normalize=True)) - linewidths.append(attr.get("edgewidth", 1.0)) - - patches.append(Polygon([point[0:2] for point in points])) - - if text: - c = centroid_points_xy(points) - axes.text( - c[0], - c[1], - text, - fontsize=attr.get("fontsize", 10.0), - zorder=ZORDER_LABELS, - ha="center", - va="center", - color=textcolor, - ) - - coll = PatchCollection( - patches, - facecolors=facecolors, - edgecolors=edgecolors, - lw=linewidths, - zorder=ZORDER_POLYGONS, - ) - - axes.add_collection(coll) - - return coll diff --git a/src/compas_plotters/core/helpers.py b/src/compas_plotters/core/helpers.py deleted file mode 100644 index 64717bf2235..00000000000 --- a/src/compas_plotters/core/helpers.py +++ /dev/null @@ -1,200 +0,0 @@ -from numpy import asarray -from numpy import argmax -from numpy import argmin -from numpy import zeros - -from mpl_toolkits.mplot3d.art3d import Poly3DCollection - -from compas_plotters.core.utilities import assert_axes_dimension - - -class Axes2D(object): - """Definition of a 2D Axes object. - - Parameters - ---------- - origin : tuple or list - X and Y coordinates for the origin. - vectors : list - The X and Y axes. - - Attributes - ---------- - origin : tuple or list - X and Y coordinates for the origin. - vectors : list - The X and Y axes. - - """ - - def __init__(self, origin, vectors): - """Initializes the Axes2D object""" - self.origin = asarray(origin) - self.vectors = asarray(vectors) - - def plot(self, axes): - """Plots the axes object - - Parameters - ---------- - axes : object - The matplotlib axes object. - - """ - assert_axes_dimension(axes, 2) - o = self.origin - xy = self.vectors - axes.plot([o[0, 0], o[0, 0] + xy[0, 0]], [o[0, 1], o[0, 1] + xy[0, 1]], "r-") - axes.plot([o[0, 0], o[0, 0] + xy[1, 0]], [o[0, 1], o[0, 1] + xy[1, 1]], "g-") - - -class Axes3D(object): - """Definition of a 3D Axes object. - - Parameters - ---------- - origin : tuple or list - X, Y and Z coordinates for the origin. - vectors : list - The X, Y and Z axes. - - Attributes - ---------- - origin : tuple or list - X, Y and Z coordinates for the origin. - vectors : list - The X, Y and Z axes. - - """ - - def __init__(self, origin, vectors, colors=None): - """Initializes the Axes3D object""" - self.origin = asarray(origin) - self.vectors = asarray(vectors) - if not colors: - colors = ("r", "g", "b") - self.colors = colors - - def plot(self, axes): - """Plots the axes object - - Parameters - ---------- - axes : object - The matplotlib axes object. - """ - assert_axes_dimension(axes, 3) - o = self.origin - xyz = self.vectors - axes.plot( - [o[0, 0], o[0, 0] + xyz[0, 0]], - [o[0, 1], o[0, 1] + xyz[0, 1]], - [o[0, 2], o[0, 2] + xyz[0, 2]], - "{0}-".format(self.colors[0]), - linewidth=3, - ) - axes.plot( - [o[0, 0], o[0, 0] + xyz[1, 0]], - [o[0, 1], o[0, 1] + xyz[1, 1]], - [o[0, 2], o[0, 2] + xyz[1, 2]], - "{0}-".format(self.colors[1]), - linewidth=3, - ) - axes.plot( - [o[0, 0], o[0, 0] + xyz[2, 0]], - [o[0, 1], o[0, 1] + xyz[2, 1]], - [o[0, 2], o[0, 2] + xyz[2, 2]], - "{0}-".format(self.colors[2]), - linewidth=3, - ) - - -class Bounds(object): - """""" - - def __init__(self, points): - self.points = asarray(points) - - def plot(self, axes): - assert_axes_dimension(axes, 3) - xmin, ymin, zmin = argmin(self.points, axis=0) - xmax, ymax, zmax = argmax(self.points, axis=0) - xspan = self.points[xmax, 0] - self.points[xmin, 0] - yspan = self.points[ymax, 1] - self.points[ymin, 1] - zspan = self.points[zmax, 2] - self.points[zmin, 2] - span = max(xspan, yspan, zspan) - axes.plot([self.points[xmin, 0]], [self.points[ymin, 1]], [self.points[zmin, 2]], "w") - axes.plot( - [self.points[xmin, 0] + span], - [self.points[ymin, 1] + span], - [self.points[zmin, 2] + span], - "w", - ) - - -class Box(object): - """""" - - def __init__(self, corners): - self.corners = corners - self.faces = [ - [0, 1, 2, 3], - [4, 7, 6, 5], - [1, 5, 6, 2], - [0, 4, 5, 1], - [0, 3, 7, 4], - [2, 6, 7, 3], - ] - - def plot(self, axes): - assert_axes_dimension(axes, 3) - rec = [[self.corners[index] for index in face] for face in self.faces] - rec_coll = Poly3DCollection(rec) - rec_coll.set_facecolors([(1.0, 0.0, 0.0) for face in self.faces]) - rec_coll.set_alpha(0.2) - axes.add_collection3d(rec_coll) - - -class Cloud2D(object): - """""" - - def __init__(self, cloud): - cloud = asarray(cloud) - cols = min(2, cloud.shape[1]) - self.cloud = zeros((cloud.shape[0], 2)) - self.cloud[:, :cols] = cloud[:, :cols] - - def plot(self, axes): - x = self.cloud[:, 0] - y = self.cloud[:, 1] - axes.plot(x, y, "o", color=(1.0, 1.0, 1.0)) - - -class Cloud3D(object): - """""" - - def __init__(self, cloud): - cloud = asarray(cloud) - cols = min(3, cloud.shape[1]) - self.cloud = zeros((cloud.shape[0], 3)) - self.cloud[:, :cols] = cloud[:, :cols] - - def plot(self, axes): - x = self.cloud[:, 0] - y = self.cloud[:, 1] - z = self.cloud[:, 2] - axes.plot(x, y, z, "o", color=(0.7, 0.7, 0.7)) - - -class Hull(object): - """""" - - def __init__(self, hull): - self.vertices = hull.points - self.faces = hull.simplices - - def plot(self, axes): - tri = [[self.vertices[index] for index in face] for face in self.faces] - tri_coll = Poly3DCollection(tri) - tri_coll.set_facecolors([(0.0, 1.0, 0.0) for face in self.faces]) - axes.add_collection3d(tri_coll) diff --git a/src/compas_plotters/core/utilities.py b/src/compas_plotters/core/utilities.py deleted file mode 100644 index eef1399ecc6..00000000000 --- a/src/compas_plotters/core/utilities.py +++ /dev/null @@ -1,63 +0,0 @@ -def get_axes_dimension(axes): - """Returns the number of dimensions of a matplotlib axes object. - - Parameters - ---------- - axes : object - The matplotlib axes object. - - Returns - ------- - int - The number of dimensions of a matplotlib axes object. - """ - if hasattr(axes, "get_zlim"): - return 3 - else: - return 2 - - -def assert_axes_dimension(axes, dim): - """Asserts if the number of dimensions of a matplotlib axes equals a given dim. - - Parameters - ---------- - axes : object - The matplotlib axes object to assert. - dim : int - The numbers of dimensions to assert with. - - Returns - ------- - bool - True if the axes object has dim dimensions. - """ - assert get_axes_dimension(axes) == dim, "The provided axes are not {0}D.".format(dim) - - -def width_to_dict(width, dictkeys, defval=None): - width = width or defval - if isinstance(width, (int, float)): - return dict((key, width) for key in dictkeys) - if isinstance(width, dict): - for k, w in width.items(): - if isinstance(w, (int, float)): - width[k] = w - return dict((key, width.get(key, defval)) for key in dictkeys) - raise Exception("This is not a valid width format: {0}".format(type(width))) - - -def size_to_sizedict(size, dictkeys, defval=None): - size = size or defval - if isinstance(size, (int, float)): - return dict((key, size) for key in dictkeys) - if isinstance(size, dict): - for k, s in size.items(): - if isinstance(s, (int, float)): - size[k] = s - return dict((key, size.get(key, defval)) for key in dictkeys) - raise Exception("This is not a valid size format: {0}".format(type(size))) - - -def synchronize_scale_axes(axes): - pass diff --git a/src/compas_plotters/plotter.py b/src/compas_plotters/plotter.py deleted file mode 100644 index bb4572201d4..00000000000 --- a/src/compas_plotters/plotter.py +++ /dev/null @@ -1,442 +0,0 @@ -""" -******************************************************************************** -plotter -******************************************************************************** - -.. currentmodule:: compas_plotters.plotter - -Classes -======= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - Plotter - -""" - -import os -from typing import Callable, Optional, Tuple, List, Union -from typing_extensions import Literal -import matplotlib -import matplotlib.pyplot as plt -import tempfile -from PIL import Image - -import compas -import compas.geometry -import compas.datastructures -from compas.geometry import allclose -from .artists import PlotterArtist - - -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: - """Plotter for the visualization of COMPAS geometry. - - Parameters - ---------- - view : tuple[tuple[float, float], tuple[float, float]], optional - The area of the axes that should be zoomed into view. - figsize : tuple[float, float], optional - Size of the figure in inches. - dpi : float, optional - Resolution of the figure in "dots per inch". - bgcolor : tuple[float, float, float], optional - Background color for the figure canvas. - show_axes : bool, optional - If True, show the axes of the figure. - zstack : {'natural', 'zorder'}, optional - If ``'natural'``, the drawing elements appear in the order they were added. - If ``'natural'``, the drawing elements are added based on their `zorder`. - - Attributes - ---------- - viewbox : tuple[tuple[float, float], tuple[float, float]] - X min-max and Y min-max of the area of the axes that is zoomed into view. - axes : matplotlib.axes.Axes, read-only - `matplotlib` axes object used by the figure. - For more info, see the documentation of the Axes class ([1]_) and the axis and tick API ([2]_). - figure : matplotlib.figure.Figure, read-only - `matplotlib` figure instance. - For more info, see the figure API ([3]_). - bgcolor : tuple[float, float, float] - Background color of the figure. - title : str - Title of the plot. - artists : list[:class:`~compas_plotters.artists.PlotterArtist`] - Artists that should be included in the plot. - - Class Attributes - ---------------- - fontsize : int - Default fontsize used by the plotter. - - References - ---------- - .. [1] https://matplotlib.org/api/axes_api.html - .. [2] https://matplotlib.org/api/axis_api.html - .. [3] https://matplotlib.org/api/figure_api.html - - Examples - -------- - >>> from compas.geometry import Point, Plane, Circle - >>> from compas_plotters.plotter import Plotter - - Create a plotter instance. - - >>> plotter = Plotter() - - Add COMPAS objects. - - >>> plotter.add(Point(0, 0, 0)) # doctest: +SKIP - # doctest: +SKIP - >>> plotter.add(Circle(Plane.worldXY(), 1.0)) # doctest: +SKIP - # doctest: +SKIP - - """ - - fontsize = 12 - - def __init__( - self, - view: Tuple[Tuple[float, float], Tuple[float, float]] = ( - (-8.0, 16.0), - (-5.0, 10.0), - ), - 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, - zstack: Literal["natural", "zorder"] = "zorder", - ): - 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 - self.zstack = zstack - - @property - def viewbox(self) -> Tuple[Tuple[float, float], Tuple[float, float]]: - return self._viewbox - - @viewbox.setter - def viewbox(self, view: Tuple[Tuple[float, float], Tuple[float, float]]): - xlim, ylim = view - xmin, xmax = xlim - ymin, ymax = ylim - self._viewbox = (xmin, xmax), (ymin, ymax) - - @property - def axes(self) -> matplotlib.axes.Axes: - 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) - 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) -> matplotlib.figure.Figure: - return self.axes.get_figure() - - @property - def bgcolor(self) -> str: - return self._bgcolor - - @bgcolor.setter - def bgcolor(self, value: Union[str, Tuple[float, float, float]]): - self._bgcolor = value - self.figure.set_facecolor(value) - - @property - def title(self) -> str: - return self.figure.canvas.get_window_title() - - @title.setter - def title(self, value: str): - self.figure.canvas.set_window_title(value) - - @property - def artists(self) -> List[PlotterArtist]: - return self._artists - - @artists.setter - def artists(self, artists: List[PlotterArtist]): - self._artists = artists - - # ========================================================================= - # Methods - # ========================================================================= - - def pause(self, pause: float) -> None: - """Pause plotting during the specified interval. - - Parameters - ---------- - pause: float - The duration of the pause in seconds. - """ - if pause: - plt.pause(pause) - - def zoom_extents(self, padding: Optional[int] = None) -> None: - """Zoom the view to the bounding box of all objects. - - Parameters - ---------- - padding : int, optional - Extra padding around the bounding box of all objects. - """ - padding = padding or 0.0 - 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) - xdiff = xmax - xmin - ydiff = ymax - ymin - - xmin = xmin - 0.1 * xdiff - padding - xmax = xmax + 0.1 * xdiff + padding - ymin = ymin - 0.1 * ydiff - padding - ymax = ymax + 0.1 * ydiff + padding - - xspan = xmax - xmin - yspan = ymax - ymin - data_aspect = xspan / yspan - - if data_aspect < fig_aspect: - scale = fig_aspect / data_aspect - xpad = (xspan * (scale - 1.0)) / 2.0 - xmin -= xpad - xmax += xpad - else: - scale = data_aspect / fig_aspect - ypad = (yspan * (scale - 1.0)) / 2.0 - ymin -= ypad - ymax += ypad - - assert allclose([fig_aspect], [(xmax - xmin) / (ymax - ymin)]) - - xlim = [xmin, xmax] - ylim = [ymin, ymax] - self.viewbox = (xlim, ylim) - self.axes.set_xlim(*xlim) - self.axes.set_ylim(*ylim) - self.axes.autoscale_view() - - def add( - self, - item: Union[ - compas.geometry.Geometry, - compas.datastructures.Network, - compas.datastructures.Mesh, - ], - artist: Optional[PlotterArtist] = None, - **kwargs, - ) -> PlotterArtist: - """Add a COMPAS geometry object or data structure to the plot. - - Parameters - ---------- - item - A COMPAS geometric primitive, network, or mesh. - artist - Type of artist to use for drawing. - - Returns - ------- - :class:`PlotterArtist` - - """ - if not artist: - if self.zstack == "natural": - zorder = 1000 + len(self._artists) * 100 - artist = PlotterArtist(item, plotter=self, zorder=zorder, context="Plotter", **kwargs) - else: - artist = PlotterArtist(item, plotter=self, context="Plotter", **kwargs) - artist.draw() - self._artists.append(artist) - return 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: - artist = self.add(item, **kwargs) - artists.append(artist) - return artists - - def find( - self, - item: Union[ - compas.geometry.Geometry, - compas.datastructures.Network, - compas.datastructures.Mesh, - ], - ) -> PlotterArtist: - """Find a geometry object or data structure in the plot.""" - for artist in self._artists: - if item is artist.item: - return artist - - def register_listener(self, listener: Callable) -> None: - """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: Optional[float] = None) -> None: - """Draw all objects included in the plot.""" - self.figure.canvas.draw() - self.figure.canvas.flush_events() - if pause: - plt.pause(pause) - - def redraw(self, pause: Optional[float] = None) -> None: - """Updates and pauses the plot. - - Parameters - ---------- - pause : float - Amount 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) -> None: - """Displays the plot.""" - self.draw() - plt.show() - - def save(self, filepath: str, **kwargs) -> None: - """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) - - def on( - self, - interval: int = None, - frames: int = None, - record: bool = False, - recording: str = None, - dpi: int = 150, - ) -> Callable: - """Method for decorating callback functions in dynamic plots.""" - if record: - if not recording: - raise Exception("Please provide a path for the recording.") - - def outer(func: Callable): - if record: - with tempfile.TemporaryDirectory() as dirpath: - paths = [] - for f in range(frames): - func(f) - self.redraw(pause=interval) - if record: - filepath = os.path.join(dirpath, f"frame-{f}.png") - paths.append(filepath) - self.save(filepath, dpi=dpi) - images = [] - for path in paths: - images.append(Image.open(path)) - images[0].save( - recording, - save_all=True, - append_images=images[1:], - optimize=False, - duration=interval * 1000, - loop=0, - ) - else: - for f in range(frames): - func(f) - self.redraw(pause=interval) - - return outer diff --git a/src/compas_rhino/artists/__init__.py b/src/compas_rhino/artists/__init__.py index 2f2d172b0c4..59e73d52ad4 100644 --- a/src/compas_rhino/artists/__init__.py +++ b/src/compas_rhino/artists/__init__.py @@ -141,8 +141,6 @@ from .surfaceartist import SurfaceArtist from .brepartist import BrepArtist -BaseArtist = RhinoArtist - @plugin(category="drawing-utils", pluggable_name="clear", requires=["Rhino"]) def clear_rhino(): @@ -182,7 +180,6 @@ def register_artists(): __all__ = [ - "BaseArtist", "RhinoArtist", "CircleArtist", "FrameArtist", diff --git a/src/compas_rhino/artists/_helpers.py b/src/compas_rhino/artists/_helpers.py new file mode 100644 index 00000000000..8a33954a680 --- /dev/null +++ b/src/compas_rhino/artists/_helpers.py @@ -0,0 +1,55 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from System.Drawing.Color import FromArgb # type: ignore +from Rhino.DocObjects.ObjectColorSource import ColorFromObject # type: ignore +from Rhino.DocObjects import ObjectAttributes # type: ignore +from Rhino.DocObjects.ObjectDecoration import EndArrowhead # type: ignore +from Rhino.DocObjects.ObjectDecoration import StartArrowhead # type: ignore + +import rhinoscriptsyntax as rs # type: ignore +import scriptcontext as sc # type: ignore + +from compas_rhino import create_layers_from_path + +try: + find_layer_by_fullpath = sc.doc.Layers.FindByFullPath +except SystemError: + find_layer_by_fullpath = None + + +def ensure_layer(layerpath): + if not rs.IsLayer(layerpath): + create_layers_from_path(layerpath) + if find_layer_by_fullpath: + index = find_layer_by_fullpath(layerpath, True) + else: + index = 0 + return index + + +def attributes(name=None, color=None, layer=None, arrow=None): + attributes = ObjectAttributes() + if name: + attributes.Name = name + if color: + attributes.ObjectColor = FromArgb(*color.rgb255) + attributes.ColorSource = ColorFromObject + if layer: + attributes.LayerIndex = ensure_layer(layer) + if arrow == "end": + attributes.ObjectDecoration = EndArrowhead + elif arrow == "start": + attributes.ObjectDecoration = StartArrowhead + return attributes + + +def ngon(v): + if v < 3: + return + if v == 3: + return [0, 1, 2, 2] + if v == 4: + return [0, 1, 2, 3] + return list(range(v)) diff --git a/src/compas_rhino/artists/artist.py b/src/compas_rhino/artists/artist.py index 1edad3e4d13..8ce1a31937e 100644 --- a/src/compas_rhino/artists/artist.py +++ b/src/compas_rhino/artists/artist.py @@ -2,6 +2,8 @@ from __future__ import absolute_import from __future__ import division +import scriptcontext as sc # type: ignore + import compas_rhino from compas.artists import Artist @@ -15,7 +17,6 @@ class RhinoArtist(Artist): A layer name. **kwargs : dict, optional Additional keyword arguments. - See :class:`Artist` for more info. """ @@ -23,6 +24,45 @@ def __init__(self, layer=None, **kwargs): super(RhinoArtist, self).__init__(**kwargs) self.layer = layer + def get_group(self, name): + """Find the group with the given name, or create a new one. + + Parameters + ---------- + name : str + The name of the group. + + Returns + ------- + :rhino:`Rhino.DocObjects.Group` + + """ + group = sc.doc.Groups.FindName(name) + if not group: + if sc.doc.Groups.Add(name) < 0: + raise Exception("Failed to add group: {}".format(name)) + group = sc.doc.Groups.FindName(name) + return group + + def add_to_group(self, name, guids): + """Add the objects to the group. + + Parameters + ---------- + name : str + The name of the group. + guids : list[System.Guid] + A list of GUIDs. + + Returns + ------- + None + + """ + group = self.get_group(name) + if group: + sc.doc.Groups.AddToGroup(group.Index, guids) + def clear_layer(self): """Clear the layer of the artist. diff --git a/src/compas_rhino/artists/boxartist.py b/src/compas_rhino/artists/boxartist.py index ce6606031dd..f13f23e475d 100644 --- a/src/compas_rhino/artists/boxartist.py +++ b/src/compas_rhino/artists/boxartist.py @@ -2,54 +2,52 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import ShapeArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import box_to_rhino +from compas_rhino.conversions import transformation_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class BoxArtist(RhinoArtist, ShapeArtist): +class BoxArtist(RhinoArtist, GeometryArtist): """Artist for drawing box shapes. Parameters ---------- box : :class:`~compas.geometry.Box` A COMPAS box. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`. """ - def __init__(self, box, layer=None, **kwargs): - super(BoxArtist, self).__init__(shape=box, layer=layer, **kwargs) + def __init__(self, box, **kwargs): + super(BoxArtist, self).__init__(geometry=box, **kwargs) def draw(self, color=None): """Draw the box associated with the artist. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the box. - Default is :attr:`compas.artists.ShapeArtist.color`. Returns ------- - list[System.Guid] - The GUIDs of the objects created in Rhino. + System.Guid + The GUID of the object created in Rhino. """ color = Color.coerce(color) or self.color - vertices = [list(vertex) for vertex in self.shape.vertices] - faces = self.shape.faces - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=self.layer, - name=self.shape.name, - color=color.rgb255, - disjoint=True, - ) - return [guid] + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + geometry = box_to_rhino(self.geometry) + + if self.transformation: + transformation = transformation_to_rhino(self.transformation) + geometry.Transform(transformation) + + return sc.doc.Objects.AddBox(geometry, attr) diff --git a/src/compas_rhino/artists/brepartist.py b/src/compas_rhino/artists/brepartist.py index 5d3e760e4a8..0557b4e5e60 100644 --- a/src/compas_rhino/artists/brepartist.py +++ b/src/compas_rhino/artists/brepartist.py @@ -1,28 +1,50 @@ -import compas_rhino +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import scriptcontext as sc # type: ignore + +from compas.colors import Color +from compas_rhino.conversions import brep_to_rhino +from compas_rhino.conversions import transformation_to_rhino +from compas.artists import GeometryArtist from .artist import RhinoArtist +from ._helpers import attributes -class BrepArtist(RhinoArtist): +class BrepArtist(RhinoArtist, GeometryArtist): """An artist for drawing a RhinoBrep. Parameters - ========== + ---------- brep : :class:`~compas_rhino.geometry.RhinoBrep` The Brep to draw. """ def __init__(self, brep, **kwargs): - super(BrepArtist, self).__init__(**kwargs) - self._brep = brep + super(BrepArtist, self).__init__(geometry=brep, **kwargs) def draw(self, color=None): """Bakes the Brep into the current document + Parameters + ---------- + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional + The RGB color of the Brep. + Returns ------- - list(:rhino:`System.Guid`) + System.Guid The guid of the baked Brep. """ - return [compas_rhino.draw_brep(self._brep, color)] + color = Color.coerce(color) or self.color + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + geometry = brep_to_rhino(self.geometry) + + if self.transformation: + geometry.Transform(transformation_to_rhino(self.transformation)) + + return sc.doc.Objects.AddBrep(geometry, attr) diff --git a/src/compas_rhino/artists/capsuleartist.py b/src/compas_rhino/artists/capsuleartist.py index 5a8df968495..65696b44e9f 100644 --- a/src/compas_rhino/artists/capsuleartist.py +++ b/src/compas_rhino/artists/capsuleartist.py @@ -2,44 +2,38 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import ShapeArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import capsule_to_rhino_brep +from compas_rhino.conversions import transformation_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class CapsuleArtist(RhinoArtist, ShapeArtist): +class CapsuleArtist(RhinoArtist, GeometryArtist): """Artist for drawing capsule shapes. Parameters ---------- capsule : :class:`~compas.geometry.Capsule` A COMPAS capsule. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`. """ - def __init__(self, capsule, layer=None, **kwargs): - super(CapsuleArtist, self).__init__(shape=capsule, layer=layer, **kwargs) + def __init__(self, capsule, **kwargs): + super(CapsuleArtist, self).__init__(geometry=capsule, **kwargs) - def draw(self, color=None, u=None, v=None): + def draw(self, color=None): """Draw the capsule associated with the artist. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the capsule. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`~CapsuleArtist.u`. - v : int, optional - Number of faces in the "v" direction. - Default is :attr:`CapsuleArtist.v`. Returns ------- @@ -48,16 +42,14 @@ def draw(self, color=None, u=None, v=None): """ color = Color.coerce(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] - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=self.layer, - name=self.shape.name, - color=color.rgb255, - disjoint=True, - ) - return [guid] + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + breps = capsule_to_rhino_brep(self.geometry) + + if self.transformation: + transformation = transformation_to_rhino(self.transformation) + for geometry in breps: + geometry.Transform(transformation) + + guids = [sc.doc.Objects.AddBrep(brep, attr) for brep in breps] + return guids diff --git a/src/compas_rhino/artists/circleartist.py b/src/compas_rhino/artists/circleartist.py index 48755a94bf7..ea781c16ca1 100644 --- a/src/compas_rhino/artists/circleartist.py +++ b/src/compas_rhino/artists/circleartist.py @@ -2,78 +2,53 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.geometry import add_vectors -from compas.artists import PrimitiveArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import circle_to_rhino + +# from compas_rhino.conversions import point_to_rhino +from compas_rhino.conversions import transformation_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class CircleArtist(RhinoArtist, PrimitiveArtist): +class CircleArtist(RhinoArtist, GeometryArtist): """Artist for drawing circles. Parameters ---------- circle : :class:`~compas.geometry.Circle` A COMPAS circle. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`. """ - def __init__(self, circle, layer=None, **kwargs): - super(CircleArtist, self).__init__(primitive=circle, layer=layer, **kwargs) + def __init__(self, circle, **kwargs): + super(CircleArtist, self).__init__(geometry=circle, **kwargs) - def draw(self, color=None, show_point=False, show_normal=False): + def draw(self, color=None): """Draw the circle. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the circle. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - show_point : bool, optional - If True, draw the center point of the circle. - show_normal : bool, optional - If True, draw the normal vector of the circle. Returns ------- - list[System.Guid] - The GUIDs of the created Rhino objects. + System.Guid + The GUID of the created Rhino object. """ color = Color.coerce(color) or self.color - color = color.rgb255 - point = list(self.primitive.plane.point) - normal = list(self.primitive.plane.normal) - plane = point, normal - radius = self.primitive.radius - guids = [] - if show_point: - points = [{"pos": point, "color": 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": color, - "name": self.primitive.name, - } - ] - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - circles = [ - { - "plane": plane, - "radius": radius, - "color": color, - "name": self.primitive.name, - } - ] - guids += compas_rhino.draw_circles(circles, layer=self.layer, clear=False, redraw=False) - return guids + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + geometry = circle_to_rhino(self.geometry) + + if self.transformation: + geometry.Transform(transformation_to_rhino(self.transformation)) + + return sc.doc.Objects.AddCircle(geometry, attr) diff --git a/src/compas_rhino/artists/coneartist.py b/src/compas_rhino/artists/coneartist.py index 81f75953e3e..5ff48444b16 100644 --- a/src/compas_rhino/artists/coneartist.py +++ b/src/compas_rhino/artists/coneartist.py @@ -2,58 +2,51 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import ShapeArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import cone_to_rhino_brep +from compas_rhino.conversions import transformation_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class ConeArtist(RhinoArtist, ShapeArtist): +class ConeArtist(RhinoArtist, GeometryArtist): """Artist for drawing cone shapes. Parameters ---------- shape : :class:`~compas.geometry.Cone` A COMPAS cone. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`. """ - def __init__(self, cone, layer=None, **kwargs): - super(ConeArtist, self).__init__(shape=cone, layer=layer, **kwargs) + def __init__(self, cone, **kwargs): + super(ConeArtist, self).__init__(geometry=cone, **kwargs) - def draw(self, color=None, u=None): + def draw(self, color=None): """Draw the cone associated with the artist. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the cone. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`ConeArtist.u`. Returns ------- - list[System.Guid] - The GUIDs of the objects created in Rhino. + System.Guid + The GUID of the object created in Rhino. """ color = Color.coerce(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] - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=self.layer, - name=self.shape.name, - color=color.rgb255, - disjoint=True, - ) - return [guid] + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + geometry = cone_to_rhino_brep(self.geometry) + + if self.transformation: + geometry.Transform(transformation_to_rhino(self.transformation)) + + return sc.doc.Objects.AddBrep(geometry, attr) diff --git a/src/compas_rhino/artists/curveartist.py b/src/compas_rhino/artists/curveartist.py index aa63a5bea68..60f24b65faf 100644 --- a/src/compas_rhino/artists/curveartist.py +++ b/src/compas_rhino/artists/curveartist.py @@ -2,45 +2,51 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import CurveArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import curve_to_rhino +from compas_rhino.conversions import transformation_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class CurveArtist(RhinoArtist, CurveArtist): +class CurveArtist(RhinoArtist, GeometryArtist): """Artist for drawing curves. Parameters ---------- curve : :class:`~compas.geometry.Curve` A COMPAS curve. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`~compas.artists.CurveArtist`. """ - def __init__(self, curve, layer=None, **kwargs): - super(CurveArtist, self).__init__(curve=curve, layer=layer, **kwargs) + def __init__(self, curve, **kwargs): + super(CurveArtist, self).__init__(geometry=curve, **kwargs) def draw(self, color=None): """Draw the curve. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the curve. - The default color is :attr:`compas.artists.CurveArtist.color`. Returns ------- - list[System.Guid] - The GUIDs of the created Rhino objects. + System.Guid + The GUID of the created Rhino object. """ color = Color.coerce(color) or self.color - curves = [{"curve": self.curve, "color": color.rgb255, "name": self.curve.name}] - return compas_rhino.draw_curves(curves, layer=self.layer, clear=False, redraw=False) + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + geometry = curve_to_rhino(self.geometry) + + if self.transformation: + geometry.Transform(transformation_to_rhino(self.transformation)) + + return sc.doc.Objects.AddCurve(geometry, attr) diff --git a/src/compas_rhino/artists/cylinderartist.py b/src/compas_rhino/artists/cylinderartist.py index 9e8c795b382..cfa5b102fe8 100644 --- a/src/compas_rhino/artists/cylinderartist.py +++ b/src/compas_rhino/artists/cylinderartist.py @@ -2,58 +2,51 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import ShapeArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import cylinder_to_rhino_brep +from compas_rhino.conversions import transformation_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class CylinderArtist(RhinoArtist, ShapeArtist): +class CylinderArtist(RhinoArtist, GeometryArtist): """Artist for drawing cylinder shapes. Parameters ---------- cylinder : :class:`~compas.geometry.Cylinder` A COMPAS cylinder. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`. """ - def __init__(self, cylinder, layer=None, **kwargs): - super(CylinderArtist, self).__init__(shape=cylinder, layer=layer, **kwargs) + def __init__(self, cylinder, **kwargs): + super(CylinderArtist, self).__init__(geometry=cylinder, **kwargs) - def draw(self, color=None, u=None): + def draw(self, color=None): """Draw the cylinder associated with the artist. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the cylinder. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`CylinderArtist.u`. Returns ------- - list[System.Guid] - The GUIDs of the objects created in Rhino. + System.Guid + The GUID of the object created in Rhino. """ color = Color.coerce(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] - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=self.layer, - name=self.shape.name, - color=color.rgb255, - disjoint=True, - ) - return [guid] + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + geometry = cylinder_to_rhino_brep(self.geometry) + + if self.transformation: + geometry.Transform(transformation_to_rhino(self.transformation)) + + return sc.doc.Objects.AddBrep(geometry, attr) diff --git a/src/compas_rhino/artists/frameartist.py b/src/compas_rhino/artists/frameartist.py index 2af86a83e33..2c9d863630a 100644 --- a/src/compas_rhino/artists/frameartist.py +++ b/src/compas_rhino/artists/frameartist.py @@ -2,13 +2,16 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import PrimitiveArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import point_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class FrameArtist(RhinoArtist, PrimitiveArtist): +class FrameArtist(RhinoArtist, GeometryArtist): """Artist for drawing frames. Parameters @@ -17,11 +20,8 @@ class FrameArtist(RhinoArtist, PrimitiveArtist): A COMPAS frame. scale: float, optional Scale factor that controls the length of the axes. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`. Attributes ---------- @@ -39,8 +39,8 @@ class FrameArtist(RhinoArtist, PrimitiveArtist): """ - def __init__(self, frame, layer=None, scale=1.0, **kwargs): - super(FrameArtist, self).__init__(primitive=frame, layer=layer, **kwargs) + def __init__(self, frame, scale=1.0, **kwargs): + super(FrameArtist, self).__init__(geometry=frame, **kwargs) self.scale = scale or 1.0 self.color_origin = Color.black() self.color_xaxis = Color.red() @@ -56,33 +56,36 @@ def draw(self): The GUIDs of the created Rhino objects. """ - points = [] - lines = [] - origin = list(self.primitive.point) - X = list(self.primitive.point + self.primitive.xaxis.scaled(self.scale)) - Y = list(self.primitive.point + self.primitive.yaxis.scaled(self.scale)) - Z = list(self.primitive.point + self.primitive.zaxis.scaled(self.scale)) - points = [{"pos": origin, "color": self.color_origin.rgb255}] - lines = [ - { - "start": origin, - "end": X, - "color": self.color_xaxis.rgb255, - "arrow": "end", - }, - { - "start": origin, - "end": Y, - "color": self.color_yaxis.rgb255, - "arrow": "end", - }, - { - "start": origin, - "end": Z, - "color": self.color_zaxis.rgb255, - "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) + guids = [] + + attr = attributes(name=self.geometry.name, color=self.color_origin, layer=self.layer) + guid = sc.doc.Objects.AddPoint(point_to_rhino(self.geometry.point), attr) + guids.append(guid) + + attr = attributes(name=self.geometry.name, color=self.color_xaxis, layer=self.layer, arrow="end") + guid = sc.doc.Objects.AddLine( + point_to_rhino(self.geometry.point), + point_to_rhino(self.geometry.point + self.geometry.xaxis.scaled(self.scale)), + attr, + ) + guids.append(guid) + + attr = attributes(name=self.geometry.name, color=self.color_yaxis, layer=self.layer, arrow="end") + guid = sc.doc.Objects.AddLine( + point_to_rhino(self.geometry.point), + point_to_rhino(self.geometry.point + self.geometry.yaxis.scaled(self.scale)), + attr, + ) + guids.append(guid) + + attr = attributes(name=self.geometry.name, color=self.color_zaxis, layer=self.layer, arrow="end") + guid = sc.doc.Objects.AddLine( + point_to_rhino(self.geometry.point), + point_to_rhino(self.geometry.point + self.geometry.zaxis.scaled(self.scale)), + attr, + ) + guids.append(guid) + + self.add_to_group("Frame.{}".format(self.geometry.name), guids) + return guids diff --git a/src/compas_rhino/artists/lineartist.py b/src/compas_rhino/artists/lineartist.py index 7a1b7fd0dbb..a0fe6b6bcd1 100644 --- a/src/compas_rhino/artists/lineartist.py +++ b/src/compas_rhino/artists/lineartist.py @@ -2,58 +2,47 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import PrimitiveArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import line_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class LineArtist(RhinoArtist, PrimitiveArtist): +class LineArtist(RhinoArtist, GeometryArtist): """Artist for drawing lines. Parameters ---------- line : :class:`~compas.geometry.Line` A COMPAS line. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`. """ - def __init__(self, line, layer=None, **kwargs): - super(LineArtist, self).__init__(primitive=line, layer=layer, **kwargs) + def __init__(self, line, **kwargs): + super(LineArtist, self).__init__(geometry=line, **kwargs) - def draw(self, color=None, show_points=False): + def draw(self, color=None): """Draw the line. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the line. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - show_points : bool, optional - If True, draw the start and end point of the line. Returns ------- - list[System.Guid] - The GUIDs of the created Rhino objects. + System.Guid + The GUID of the created Rhino object. """ - start = list(self.primitive.start) - end = list(self.primitive.end) color = Color.coerce(color) or self.color - color = color.rgb255 - guids = [] - if show_points: - points = [ - {"pos": start, "color": color, "name": self.primitive.name}, - {"pos": end, "color": color, "name": self.primitive.name}, - ] - guids += compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - lines = [{"start": start, "end": end, "color": color, "name": self.primitive.name}] - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - return guids + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + geometry = line_to_rhino(self.geometry) + + return sc.doc.Objects.AddLine(geometry, attr) diff --git a/src/compas_rhino/artists/meshartist.py b/src/compas_rhino/artists/meshartist.py index 724906c6059..49a07f627b7 100644 --- a/src/compas_rhino/artists/meshartist.py +++ b/src/compas_rhino/artists/meshartist.py @@ -2,33 +2,44 @@ from __future__ import absolute_import from __future__ import division -from compas.geometry import add_vectors -from compas.geometry import scale_vector +from Rhino.Geometry import TextDot # type: ignore +import scriptcontext as sc # type: ignore + from compas.geometry import centroid_points +from compas.geometry import Point +from compas.geometry import Line +from compas.geometry import Cylinder +from compas.geometry import Sphere import compas_rhino -from compas.artists import MeshArtist +from compas.artists import MeshArtist as BaseArtist from compas.colors import Color +from compas_rhino.conversions import vertices_and_faces_to_rhino +from compas_rhino.conversions import mesh_to_rhino +from compas_rhino.conversions import point_to_rhino +from compas_rhino.conversions import line_to_rhino +from compas_rhino.conversions import cylinder_to_rhino_brep +from compas_rhino.conversions import sphere_to_rhino +from compas_rhino.conversions import transformation_to_rhino from .artist import RhinoArtist +from ._helpers import attributes +from ._helpers import ngon -class MeshArtist(RhinoArtist, MeshArtist): +class MeshArtist(RhinoArtist, BaseArtist): """Artists for drawing mesh data structures. Parameters ---------- mesh : :class:`~compas.datastructures.Mesh` A COMPAS mesh. - layer : str, optional - The name of the layer that will contain the mesh. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`MeshArtist`. """ - def __init__(self, mesh, layer=None, **kwargs): - super(MeshArtist, self).__init__(mesh=mesh, layer=layer, **kwargs) + def __init__(self, mesh, **kwargs): + super(MeshArtist, self).__init__(mesh=mesh, **kwargs) # ========================================================================== # clear @@ -42,7 +53,7 @@ def clear(self): None """ - guids = compas_rhino.get_objects(name="{}.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.*".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_vertices(self): @@ -53,7 +64,7 @@ def clear_vertices(self): None """ - guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_edges(self): @@ -64,7 +75,7 @@ def clear_edges(self): None """ - guids = compas_rhino.get_objects(name="{}.edge.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.edge.*".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_faces(self): @@ -75,7 +86,7 @@ def clear_faces(self): None """ - guids = compas_rhino.get_objects(name="{}.face.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.face.*".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_vertexnormals(self): @@ -86,7 +97,7 @@ def clear_vertexnormals(self): None """ - guids = compas_rhino.get_objects(name="{}.vertexnormal.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.vertex.*.normal".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_facenormals(self): @@ -97,7 +108,7 @@ def clear_facenormals(self): None """ - guids = compas_rhino.get_objects(name="{}.facenormal.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.face.*.normal".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_vertexlabels(self): @@ -108,7 +119,7 @@ def clear_vertexlabels(self): None """ - guids = compas_rhino.get_objects(name="{}.vertexlabel.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.vertex.*.label".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_edgelabels(self): @@ -119,7 +130,7 @@ def clear_edgelabels(self): None """ - guids = compas_rhino.get_objects(name="{}.edgelabel.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.edge.*.label".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_facelabels(self): @@ -130,14 +141,14 @@ def clear_facelabels(self): None """ - guids = compas_rhino.get_objects(name="{}.facelabel.*".format(self.mesh.name)) + guids = compas_rhino.get_objects(name="{}.face.*.label".format(self.mesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) # ========================================================================== # draw # ========================================================================== - def draw(self, color=None, disjoint=False): + def draw(self, color=None, vertexcolors=None, facecolors=None, disjoint=False): """Draw the mesh as a consolidated RhinoMesh. Parameters @@ -159,24 +170,29 @@ def draw(self, color=None, disjoint=False): Faces with more than 4 vertices will be triangulated on-the-fly. """ - self.color = 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()] - layer = self.layer - name = "{}.mesh".format(self.mesh.name) - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=layer, - name=name, - color=self.color.rgb255, + # when drawing the actual mesh + # vertex colors or face colors have to be provided as lists with colors for all vertices or faces + # when drawing the mesh as individual components (vertices, edges, faces) + # colors have to be provided as dicts that map colors to specific components + + color = Color.coerce(color) or self.color + attr = attributes(name=self.mesh.name, color=color, layer=self.layer) # type: ignore + + geometry = mesh_to_rhino( + self.mesh, + color=color, + vertexcolors=vertexcolors, + facecolors=facecolors, disjoint=disjoint, ) + + if self.transformation: + geometry.Transform(transformation_to_rhino(self.transformation)) + + guid = sc.doc.Objects.AddMesh(geometry, attr) return [guid] - def draw_vertices(self, vertices=None, color=None): + def draw_vertices(self, vertices=None, color=None, group=None): """Draw a selection of vertices. Parameters @@ -186,29 +202,35 @@ def draw_vertices(self, vertices=None, color=None): Default is None, in which case all vertices are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the vertices. - The default is the value of :attr:`MeshArtist.default_vertexcolor`. + group : str, optional + The name of a group to join the created Rhino objects in. Returns ------- list[System.Guid] - The GUIDs of the created Rhino objects. + The GUIDs of the created Rhino point objects. """ + guids = [] + self.vertex_color = color - vertices = vertices or self.vertices - vertex_xyz = self.vertex_xyz - points = [] - for vertex in vertices: - points.append( - { - "pos": vertex_xyz[vertex], - "name": "{}.vertex.{}".format(self.mesh.name, vertex), - "color": self.vertex_color[vertex].rgb255, - } - ) - return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - - def draw_edges(self, edges=None, color=None): + + for vertex in vertices or self.mesh.vertices(): # type: ignore + name = "{}.vertex.{}".format(self.mesh.name, vertex) # type: ignore + color = self.vertex_color[vertex] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + point = point_to_rhino(self.vertex_xyz[vertex]) + + guid = sc.doc.Objects.AddPoint(point, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_edges(self, edges=None, color=None, text=None, fontheight=10, fontface="Arial Regular", group=None): """Draw a selection of edges. Parameters @@ -218,31 +240,39 @@ def draw_edges(self, edges=None, color=None): The default is None, in which case all edges are drawn. color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional The color of the edges. - The default color is the value of :attr:`MeshArtist.default_edgecolor`. + text : dict[tuple[int, int], str], optional + A dictionary of edge labels as edge-text pairs. + fontheight : int, optional + Font height of the edge labels. + fontface : str, optional + Font face of the edge labels. Returns ------- list[System.Guid] - The GUIDs of the created Rhino objects. + The GUIDs of the created Rhino line objects. """ + guids = [] + 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[edge].rgb255, - "name": "{}.edge.{}-{}".format(self.mesh.name, *edge), - } - ) - guids = compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) + + for edge in edges or self.mesh.edges(): # type: ignore + name = "{}.edge.{}-{}".format(self.mesh.name, *edge) # type: ignore + color = self.edge_color[edge] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + line = Line(self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]) + + guid = sc.doc.Objects.AddLine(line_to_rhino(line), attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + return guids - def draw_faces(self, faces=None, color=None, join_faces=False): + def draw_faces(self, faces=None, color=None, group=None): """Draw a selection of faces. Parameters @@ -252,43 +282,184 @@ def draw_faces(self, faces=None, color=None, join_faces=False): The default is None, in which case all faces are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the faces. - The default color is the value of :attr:`MeshArtist.default_facecolor`. - join_faces : bool, optional - If True, join the faces into a single mesh. + text : dict[int, str], optional + A dictionary of face labels as face-text pairs. + fontheight : int, optional + Font height of the face labels. + fontface : str, optional + Font face of the face labels. Returns ------- list[System.Guid] - The GUIDs of the created Rhino objects. + The GUIDs of the created Rhino mesh objects. """ + guids = [] + self.face_color = color - faces = faces or self.faces - vertex_xyz = self.vertex_xyz - 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": self.face_color[face].rgb255, - } - ) - guids = compas_rhino.draw_faces(facets, 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)) - if color: - compas_rhino.rs.ObjectColor(guid, color) - guids = [guid] + + for face in faces or self.mesh.faces(): # type: ignore + name = "{}.face.{}".format(self.mesh.name, face) # type: ignore + color = self.face_color[face] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + vertices = [self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)] # type: ignore + facet = ngon(len(vertices)) + + if facet: + guid = sc.doc.Objects.AddMesh(vertices_and_faces_to_rhino(vertices, [facet]), attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + # ========================================================================== + # draw labels + # ========================================================================== + + def draw_vertexlabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): + """Draw labels for a selection of vertices. + + Parameters + ---------- + text : dict[int, str] + A dictionary of vertex labels as vertex-text pairs. + color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color of the vertex labels. + group : str, optional + The name of a group to join the created Rhino objects in. + fontheight : int, optional + Font height of the vertex labels. + fontface : str, optional + Font face of the vertex labels. + + Returns + ------- + list[System.Guid] + The GUIDs of the created Rhino point objects. + + """ + guids = [] + + self.vertex_color = color + + for vertex in text: + name = "{}.vertex.{}.label".format(self.mesh.name, vertex) # type: ignore + color = self.vertex_color[vertex] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + point = point_to_rhino(self.vertex_xyz[vertex]) + + dot = TextDot(str(text[vertex]), point) # type: ignore + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_edgelabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): + """Draw labels for a selection of edges. + + Parameters + ---------- + text : dict[tuple[int, int], str] + A dictionary of edge labels as edge-text pairs. + color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional + The color of the edge labels. + group : str, optional + The name of a group to join the created Rhino objects in. + fontheight : int, optional + Font height of the edge labels. + fontface : str, optional + Font face of the edge labels. + + Returns + ------- + list[System.Guid] + The GUIDs of the created Rhino point objects. + + """ + guids = [] + + self.edge_color = color + + for edge in text: + name = "{}.edge.{}-{}".format(self.mesh.name, *edge) # type: ignore + color = self.edge_color[edge] # type: ignore + attr = attributes(name="{}.label".format(name), color=color, layer=self.layer) + + line = Line(self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]) + point = point_to_rhino(line.midpoint) + + dot = TextDot(str(text[edge]), point) # type: ignore + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_facelabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): + """Draw labels for a selection of faces. + + Parameters + ---------- + text : dict[int, str] + A dictionary of face labels as face-text pairs. + color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color of the face labels. + group : str, optional + The name of a group to join the created Rhino objects in. + fontheight : int, optional + Font height of the face labels. + fontface : str, optional + Font face of the face labels. + + Returns + ------- + list[System.Guid] + The GUIDs of the created Rhino point objects. + + """ + guids = [] + + for face in text: + name = "{}.face.{}.label".format(self.mesh.name, face) # type: ignore + color = self.face_color[face] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + points = [self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)] # type: ignore + point = point_to_rhino(centroid_points(points)) # type: ignore + + dot = TextDot(str(text[face]), point) # type: ignore + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + return guids # ========================================================================== # draw normals # ========================================================================== - def draw_vertexnormals(self, vertices=None, color=(0, 255, 0), scale=1.0): + def draw_vertexnormals(self, vertices=None, color=(0, 255, 0), scale=1.0, group=None): """Draw the normals at the vertices of the mesh. Parameters @@ -300,6 +471,8 @@ def draw_vertexnormals(self, vertices=None, color=(0, 255, 0), scale=1.0): The color specification of the normal vectors. scale : float, optional Scale factor for the vertex normals. + group : str, optional + The name of a group to join the created Rhino objects in. Returns ------- @@ -307,26 +480,26 @@ def draw_vertexnormals(self, vertices=None, color=(0, 255, 0), scale=1.0): The GUIDs of the created Rhino objects. """ - color = Color.coerce(color).rgb255 - vertex_xyz = self.vertex_xyz - vertices = vertices or self.vertices - lines = [] - for vertex in vertices: - a = vertex_xyz[vertex] - n = self.mesh.vertex_normal(vertex) - b = add_vectors(a, scale_vector(n, scale)) - lines.append( - { - "start": a, - "end": b, - "color": color, - "name": "{}.vertexnormal.{}".format(self.mesh.name, vertex), - "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): + guids = [] + + color = Color.coerce(color) + + for vertex in vertices or self.mesh.vertices(): # type: ignore + name = "{}.vertex.{}.normal".format(self.mesh.name, vertex) # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + point = Point(*self.vertex_xyz[vertex]) + normal = self.mesh.vertex_normal(vertex) # type: ignore + + guid = sc.doc.Objects.AddLine(point_to_rhino(point), point_to_rhino(point + normal * scale), attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_facenormals(self, faces=None, color=(0, 255, 255), scale=1.0, group=None): """Draw the normals of the faces. Parameters @@ -338,6 +511,8 @@ def draw_facenormals(self, faces=None, color=(0, 255, 255), scale=1.0): The color specification of the normal vectors. scale : float, optional Scale factor for the face normals. + group : str, optional + The name of a group to join the created Rhino objects in. Returns ------- @@ -345,37 +520,40 @@ def draw_facenormals(self, faces=None, color=(0, 255, 255), scale=1.0): The GUIDs of the created Rhino objects. """ - color = Color.coerce(color).rgb255 - vertex_xyz = self.vertex_xyz - faces = faces or self.faces - lines = [] - for face in faces: - a = centroid_points([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( - { - "start": a, - "end": b, - "name": "{}.facenormal.{}".format(self.mesh.name, face), - "color": color, - "arrow": "end", - } - ) - return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) + guids = [] + + color = Color.coerce(color) + + for face in faces or self.mesh.faces(): # type: ignore + name = "{}.face.{}.normal".format(self.mesh.name, face) # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + point = Point(*centroid_points([self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)])) # type: ignore + normal = self.mesh.face_normal(face) # type: ignore + + guid = sc.doc.Objects.AddLine(point_to_rhino(point), point_to_rhino(point + normal * scale), attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids # ========================================================================== - # draw labels + # draw miscellaneous # ========================================================================== - def draw_vertexlabels(self, text=None): - """Draw labels for a selection vertices. + def draw_spheres(self, radius, color=None, group=None): + """Draw spheres at the vertices of the mesh. Parameters ---------- - text : dict[int, str], optional - A dictionary of vertex labels as vertex-text pairs. - The default value is None, in which case every vertex will be labelled with its identifier. + radius : dict[int, float], optional + The radius of the spheres. + color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color of the spheres. + group : str, optional + The name of a group to join the created Rhino objects in. Returns ------- @@ -383,57 +561,37 @@ def draw_vertexlabels(self, text=None): The GUIDs of the created Rhino objects. """ - self.vertex_text = text - vertex_xyz = self.vertex_xyz - labels = [] - for vertex in self.vertex_text: - labels.append( - { - "pos": vertex_xyz[vertex], - "name": "{}.vertexlabel.{}".format(self.mesh.name, vertex), - "color": self.vertex_color[vertex].rgb255, - "text": self.vertex_text[vertex], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) - - def draw_edgelabels(self, text=None): - """Draw labels for a selection of edges. + guids = [] - Parameters - ---------- - text : dict[tuple[int, int], str], 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 identifier. + self.vertex_color = color - Returns - ------- - list[System.Guid] - The GUIDs of the created Rhino objects. + for vertex in radius: + name = "{}.vertex.{}.sphere".format(self.mesh.name, vertex) # type: ignore + color = self.vertex_color[vertex] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) - """ - self.edge_text = text - vertex_xyz = self.vertex_xyz - labels = [] - for edge in self.edge_text: - labels.append( - { - "pos": centroid_points([vertex_xyz[edge[0]], vertex_xyz[edge[1]]]), - "name": "{}.edgelabel.{}-{}".format(self.mesh.name, *edge), - "color": self.edge_color[edge].rgb255, - "text": self.edge_text[edge], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) - - def draw_facelabels(self, text=None): - """Draw labels for a selection of faces. + sphere = Sphere.from_point_and_radius(self.vertex_xyz[vertex], radius[vertex]) + geometry = sphere_to_rhino(sphere) + + guid = sc.doc.Objects.AddSphere(geometry, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_pipes(self, radius, color=None, group=None): + """Draw pipes around the edges of the mesh. Parameters ---------- - text : dict[int, str], 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. + radius : dict[tuple[int, int], float] + The radius per edge. + color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional + The color of the pipes. + group : str, optional + The name of a group to join the created Rhino objects in. Returns ------- @@ -441,16 +599,23 @@ def draw_facelabels(self, text=None): The GUIDs of the created Rhino objects. """ - self.face_text = text - vertex_xyz = self.vertex_xyz - labels = [] - for face in self.face_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": self.face_color[face].rgb255, - "text": self.face_text[face], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) + guids = [] + + self.edge_color = color + + for edge in radius: + name = "{}.edge.{}-{}.pipe".format(self.mesh.name, *edge) # type: ignore + color = self.edge_color[edge] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + line = Line(self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]) + cylinder = Cylinder.from_line_and_radius(line, radius[edge]) + brep = cylinder_to_rhino_brep(cylinder) + + guid = sc.doc.Objects.AddBrep(brep, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids diff --git a/src/compas_rhino/artists/networkartist.py b/src/compas_rhino/artists/networkartist.py index 906cb672f0d..7cd757bfa68 100644 --- a/src/compas_rhino/artists/networkartist.py +++ b/src/compas_rhino/artists/networkartist.py @@ -2,30 +2,36 @@ from __future__ import absolute_import from __future__ import division +from Rhino.Geometry import TextDot # type: ignore +import scriptcontext as sc # type: ignore + import compas_rhino -from compas.geometry import centroid_points -from compas.artists import NetworkArtist +from compas.geometry import Line +from compas.geometry import Cylinder +from compas.geometry import Sphere +from compas.artists import NetworkArtist as BaseArtist +from compas_rhino.conversions import point_to_rhino +from compas_rhino.conversions import line_to_rhino +from compas_rhino.conversions import sphere_to_rhino +from compas_rhino.conversions import cylinder_to_rhino_brep from .artist import RhinoArtist +from ._helpers import attributes -class NetworkArtist(RhinoArtist, NetworkArtist): +class NetworkArtist(RhinoArtist, BaseArtist): """Artist for drawing network data structures. Parameters ---------- network : :class:`~compas.datastructures.Network` A COMPAS network. - layer : str, optional - The parent layer of the network. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`NetworkArtist`. """ - def __init__(self, network, layer=None, **kwargs): - - super(NetworkArtist, self).__init__(network=network, layer=layer, **kwargs) + def __init__(self, network, **kwargs): + super(NetworkArtist, self).__init__(network=network, **kwargs) # ========================================================================== # clear @@ -39,7 +45,7 @@ def clear(self): None """ - guids = compas_rhino.get_objects(name="{}.*".format(self.network.name)) + guids = compas_rhino.get_objects(name="{}.*".format(self.network.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_nodes(self): @@ -50,7 +56,7 @@ def clear_nodes(self): None """ - guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.network.name)) + guids = compas_rhino.get_objects(name="{}.node.*".format(self.network.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_edges(self): @@ -61,36 +67,20 @@ def clear_edges(self): None """ - guids = compas_rhino.get_objects(name="{}.edge.*".format(self.network.name)) - compas_rhino.delete_objects(guids, purge=True) - - def clear_nodelabels(self): - """Delete all node labels drawn by this artist. - - Returns - ------- - None - - """ - guids = compas_rhino.get_objects(name="{}.nodexlabel.*".format(self.network.name)) - compas_rhino.delete_objects(guids, purge=True) - - def clear_edgelabels(self): - """Delete all edge labels drawn by this artist. - - Returns - ------- - None - - """ - guids = compas_rhino.get_objects(name="{}.edgelabel.*".format(self.network.name)) + guids = compas_rhino.get_objects(name="{}.edge.*".format(self.network.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) # ========================================================================== # draw # ========================================================================== - def draw(self, nodes=None, edges=None, nodecolor=None, edgecolor=None): + def draw( + self, + nodes=None, + edges=None, + nodecolor=None, + edgecolor=None, + ): """Draw the network using the chosen visualisation settings. Parameters @@ -103,10 +93,8 @@ def draw(self, nodes=None, edges=None, nodecolor=None, edgecolor=None): The default is None, in which case all edges are drawn. nodecolor : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the nodes. - The default color is :attr:`NetworkArtist.default_nodecolor`. edgecolor : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional The color of the edges. - The default color is :attr:`NetworkArtist.default_edgecolor`. Returns ------- @@ -119,7 +107,7 @@ def draw(self, nodes=None, edges=None, nodecolor=None, edgecolor=None): guids += self.draw_edges(edges=edges, color=edgecolor) return guids - def draw_nodes(self, nodes=None, color=None): + def draw_nodes(self, nodes=None, color=None, group=None): """Draw a selection of nodes. Parameters @@ -129,29 +117,32 @@ def draw_nodes(self, nodes=None, color=None): Default is None, in which case all nodes are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional Color of the nodes. - The default color is :attr:`NetworkArtist.default_nodecolor`. Returns ------- list[System.Guid] - The GUIDs of the created Rhino objects. + The GUIDs of the created Rhino point objects. """ + guids = [] + self.node_color = color - nodes = nodes or self.nodes - node_xyz = self.node_xyz - points = [] - for node in nodes: - points.append( - { - "pos": node_xyz[node], - "name": "{}.node.{}".format(self.network.name, node), - "color": self.node_color[node].rgb255, - } - ) - return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - - def draw_edges(self, edges=None, color=None): + + for node in nodes or self.network.nodes(): # type: ignore + name = "{}.node.{}".format(self.network.name, node) # type: ignore + attr = attributes(name=name, color=self.node_color[node], layer=self.layer) # type: ignore + + point = point_to_rhino(self.node_xyz[node]) + + guid = sc.doc.Objects.AddPoint(point, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_edges(self, edges=None, color=None, group=None, show_direction=False): """Draw a selection of edges. Parameters @@ -161,7 +152,10 @@ def draw_edges(self, edges=None, color=None): The default is None, in which case all edges are drawn. color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional Color of the edges. - The default color is :attr:`NetworkArtist.default_edgecolor`. + group : str, optional + The name of a group to add the edges to. + show_direction : bool, optional + Show the direction of the edges. Returns ------- @@ -169,81 +163,201 @@ def draw_edges(self, edges=None, color=None): The GUIDs of the created Rhino objects. """ + guids = [] + + arrow = "end" if show_direction else None self.edge_color = color - edges = edges or self.edges - node_xyz = self.node_xyz - lines = [] - for edge in edges: + + for edge in edges or self.network.edges(): # type: ignore u, v = edge - lines.append( - { - "start": node_xyz[u], - "end": node_xyz[v], - "color": self.edge_color[edge].rgb255, - "name": "{}.edge.{}-{}".format(self.network.name, u, v), - } - ) - return compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - # ========================================================================== + color = self.edge_color[edge] # type: ignore + name = "{}.edge.{}-{}".format(self.network.name, u, v) # type: ignore + attr = attributes(name=name, color=color, layer=self.layer, arrow=arrow) # type: ignore + + line = Line(self.node_xyz[u], self.node_xyz[v]) + + guid = sc.doc.Objects.AddLine(line_to_rhino(line), attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + # ============================================================================= # draw labels - # ========================================================================== + # ============================================================================= - def draw_nodelabels(self, text=None): - """Draw labels for a selection nodes. + def draw_nodelabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): + """Draw labels for a selection of nodes. Parameters ---------- - text : dict[int, str], optional + text : dict[int, str] A dictionary of node labels as node-text pairs. - The default value is None, in which case every node will be labelled with its key. + color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + Color of the labels. + group : str, optional + The name of a group to add the labels to. + fontheight : float, optional + Font height of the labels. + fontface : str, optional + Font face of the labels. Returns ------- list[System.Guid] - The GUIDs of the created Rhino objects. + The GUIDs of the created Rhino text objects. """ - self.node_text = text - node_xyz = self.node_xyz - labels = [] - for node in self.node_text: - labels.append( - { - "pos": node_xyz[node], - "name": "{}.nodelabel.{}".format(self.network.name, node), - "color": self.node_color[node].rgb255, - "text": self.node_text[node], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) - - def draw_edgelabels(self, text=None): + guids = [] + + self.node_color = color + + for node in text: + name = "{}.node.{}.label".format(self.network.name, node) # type: ignore + attr = attributes(name=name, color=self.node_color[node], layer=self.layer) # type: ignore + + point = point_to_rhino(self.node_xyz[node]) + + dot = TextDot(str(text[node]), point) # type: ignore + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_edgelabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): """Draw labels for a selection of edges. Parameters ---------- - text : dict[tuple[int, int], str], optional - A dictionary of edgelabels as edge-text pairs. - The default value is None, in which case every edge will be labelled with its key. + text : dict[tuple[int, int], str] + A dictionary of edge labels as edge-text pairs. + color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional + Color of the labels. + group : str, optional + The name of a group to add the labels to. + fontheight : float, optional + Font height of the labels. + fontface : str, optional + Font face of the labels. Returns ------- list[System.Guid] - The GUIDs of the created Rhino objects. + The GUIDs of the created Rhino text objects. """ - self.edge_text = text - node_xyz = self.node_xyz - labels = [] - for edge in self.edge_text: + guids = [] + + self.edge_color = color + + for edge in text: u, v = edge - labels.append( - { - "pos": centroid_points([node_xyz[u], node_xyz[v]]), - "name": "{}.edgelabel.{}-{}".format(self.network.name, u, v), - "color": self.edge_color[edge].rgb255, - "text": self.edge_text[edge], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) + + color = self.edge_color[edge] # type: ignore + name = "{}.edge.{}-{}.label".format(self.network.name, u, v) # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + line = Line(self.node_xyz[u], self.node_xyz[v]) + point = point_to_rhino(line.midpoint) + + dot = TextDot(str(text[edge]), point) + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + # ============================================================================= + # draw miscellaneous + # ============================================================================= + + def draw_spheres(self, radius, color=None, group=None): + """Draw spheres at the vertices of the network. + + Parameters + ---------- + radius : dict[int, float], optional + The radius of the spheres. + color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color of the spheres. + group : str, optional + The name of a group to join the created Rhino objects in. + + Returns + ------- + list[System.Guid] + The GUIDs of the created Rhino objects. + + """ + guids = [] + + self.node_color = color + + for node in radius: + name = "{}.node.{}.sphere".format(self.network.name, node) # type: ignore + color = self.node_color[node] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + sphere = Sphere.from_point_and_radius(self.node_xyz[node], radius[node]) + geometry = sphere_to_rhino(sphere) + + guid = sc.doc.Objects.AddSphere(geometry, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_pipes(self, radius, color=None, group=None): + """Draw pipes around the edges of the network. + + Parameters + ---------- + radius : dict[tuple[int, int], float] + The radius per edge. + color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional + The color of the pipes. + group : str, optional + The name of a group to join the created Rhino objects in. + + Returns + ------- + list[System.Guid] + The GUIDs of the created Rhino objects. + + """ + guids = [] + + self.edge_color = color + + for edge in radius: + name = "{}.edge.{}-{}.pipe".format(self.network.name, *edge) # type: ignore + color = self.edge_color[edge] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + line = Line(self.node_xyz[edge[0]], self.node_xyz[edge[1]]) + cylinder = Cylinder.from_line_and_radius(line, radius[edge]) + geometry = cylinder_to_rhino_brep(cylinder) + + guid = sc.doc.Objects.AddBrep(geometry, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids diff --git a/src/compas_rhino/artists/planeartist.py b/src/compas_rhino/artists/planeartist.py index 2e2f679c267..b5eb788fb51 100644 --- a/src/compas_rhino/artists/planeartist.py +++ b/src/compas_rhino/artists/planeartist.py @@ -2,27 +2,24 @@ from __future__ import absolute_import from __future__ import division -from compas.artists import PrimitiveArtist +from compas.artists import GeometryArtist from .artist import RhinoArtist -class PlaneArtist(RhinoArtist, PrimitiveArtist): +class PlaneArtist(RhinoArtist, GeometryArtist): """Artist for drawing planes. Parameters ---------- plane : :class:`~compas.geometry.Plane` A COMPAS plane. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`. """ - def __init__(self, plane, layer=None, **kwargs): - super(PlaneArtist, self).__init__(primitive=plane, layer=layer, **kwargs) + def __init__(self, plane, **kwargs): + super(PlaneArtist, self).__init__(geometry=plane, **kwargs) def draw(self): """Draw the plane. diff --git a/src/compas_rhino/artists/pointartist.py b/src/compas_rhino/artists/pointartist.py index fb9ca67ff4a..caf0c8f09ef 100644 --- a/src/compas_rhino/artists/pointartist.py +++ b/src/compas_rhino/artists/pointartist.py @@ -2,52 +2,44 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import PrimitiveArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import point_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class PointArtist(RhinoArtist, PrimitiveArtist): +class PointArtist(RhinoArtist, GeometryArtist): """Artist for drawing points. Parameters ---------- point : :class:`~compas.geometry.Point` A COMPAS point. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`. """ - def __init__(self, point, layer=None, **kwargs): - super(PointArtist, self).__init__(primitive=point, layer=layer, **kwargs) + def __init__(self, point, **kwargs): + super(PointArtist, self).__init__(geometry=point, **kwargs) def draw(self, color=None): """Draw the point. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the point. - Default is :attr:`compas.artists.PrimitiveArtist.color`. Returns ------- - list[System.Guid] - The GUIDs of the created Rhino objects. + System.Guid + The GUID of the created Rhino object. """ color = Color.coerce(color) or self.color - points = [ - { - "pos": list(self.primitive), - "color": color.rgb255, - "name": self.primitive.name, - } - ] - guids = compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - return guids + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + return sc.doc.Objects.AddPoint(point_to_rhino(self.geometry), attr) diff --git a/src/compas_rhino/artists/polygonartist.py b/src/compas_rhino/artists/polygonartist.py index feefc90df93..476107cda13 100644 --- a/src/compas_rhino/artists/polygonartist.py +++ b/src/compas_rhino/artists/polygonartist.py @@ -2,44 +2,62 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import PrimitiveArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import point_to_rhino +from compas_rhino.conversions import line_to_rhino +from compas_rhino.conversions import vertices_and_faces_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class PolygonArtist(RhinoArtist, PrimitiveArtist): +class PolygonArtist(RhinoArtist, GeometryArtist): """Artist for drawing polygons. Parameters ---------- polygon : :class:`~compas.geometry.Polygon` A COMPAS polygon. - layer : str, optional - The name of the layer that will contain the mesh. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`. """ - def __init__(self, polygon, layer=None, **kwargs): - super(PolygonArtist, self).__init__(primitive=polygon, layer=layer, **kwargs) + def __init__(self, polygon, **kwargs): + super(PolygonArtist, self).__init__(geometry=polygon, **kwargs) - def draw(self, color=None, show_points=False, show_edges=False, show_face=True): + def draw(self, color=None): """Draw the polygon. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the polygon. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - show_points : bool, optional - If True, draw the corner points of the polygon. - show_edges : bool, optional - If True, draw the boundary edges of the polygon. - show_face : bool, optional - If True, draw the face of the polygon. + + Returns + ------- + System.Guid + The GUID of the created Rhino object. + + """ + color = Color.coerce(color) or self.color + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + vertices = self.geometry.points + faces = self.geometry.faces + mesh = vertices_and_faces_to_rhino(vertices, faces) + + return sc.doc.Objects.AddMesh(mesh, attr) + + def draw_vertices(self, color=None): + """Draw the polygon vertices. + + Parameters + ---------- + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional + The RGB color of the polygon vertices. Returns ------- @@ -48,24 +66,37 @@ def draw(self, color=None, show_points=False, show_edges=False, show_face=True): """ color = Color.coerce(color) or self.color - color = color.rgb255 - _points = map(list, self.primitive.points) + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + guids = [] - if show_points: - points = [{"pos": point, "color": 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": 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": color, "name": self.primitive.name}] - guids += compas_rhino.draw_faces(polygons, layer=self.layer, clear=False, redraw=False) + + for point in self.geometry.points: + guid = sc.doc.Objects.AddPoint(point_to_rhino(point), attr) + guids.append(guid) + + return guids + + def draw_edges(self, color=None): + """Draw the polygon edges. + + Parameters + ---------- + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional + The RGB color of the polygon edges. + + Returns + ------- + list[System.Guid] + The GUIDs of the created Rhino objects. + + """ + color = Color.coerce(color) or self.color + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + guids = [] + + for line in self.geometry.lines: + guid = sc.doc.Objects.AddLine(line_to_rhino(line), attr) + guids.append(guid) + return guids diff --git a/src/compas_rhino/artists/polyhedronartist.py b/src/compas_rhino/artists/polyhedronartist.py index 241af6d9ece..886efa2d07a 100644 --- a/src/compas_rhino/artists/polyhedronartist.py +++ b/src/compas_rhino/artists/polyhedronartist.py @@ -2,54 +2,48 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import ShapeArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import vertices_and_faces_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class PolyhedronArtist(RhinoArtist, ShapeArtist): +class PolyhedronArtist(RhinoArtist, GeometryArtist): """Artist for drawing polyhedron shapes. Parameters ---------- polyhedron : :class:`~compas.geometry.Polyhedron` A COMPAS polyhedron. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`. """ - def __init__(self, polyhedron, layer=None, **kwargs): - super(PolyhedronArtist, self).__init__(shape=polyhedron, layer=layer, **kwargs) + def __init__(self, polyhedron, **kwargs): + super(PolyhedronArtist, self).__init__(geometry=polyhedron, **kwargs) def draw(self, color=None): """Draw the polyhedron associated with the artist. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the polyhedron. - Default is :attr:`compas.artists.ShapeArtist.color`. Returns ------- - list[System.Guid] - The GUIDs of the objects created in Rhino. + System.Guid + The GUID of the object created in Rhino. """ color = Color.coerce(color) or self.color - vertices = [list(vertex) for vertex in self.shape.vertices] - faces = self.shape.faces - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=self.layer, - name=self.shape.name, - color=color.rgb255, - disjoint=True, - ) - return [guid] + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + vertices = [list(vertex) for vertex in self.geometry.vertices] + faces = self.geometry.faces + + return sc.doc.Objects.AddMesh(vertices_and_faces_to_rhino(vertices, faces), attr) diff --git a/src/compas_rhino/artists/polylineartist.py b/src/compas_rhino/artists/polylineartist.py index 85af4e26366..4c3f12c3568 100644 --- a/src/compas_rhino/artists/polylineartist.py +++ b/src/compas_rhino/artists/polylineartist.py @@ -2,40 +2,57 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import PrimitiveArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import point_to_rhino +from compas_rhino.conversions import polyline_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class PolylineArtist(RhinoArtist, PrimitiveArtist): +class PolylineArtist(RhinoArtist, GeometryArtist): """Artist for drawing polylines. Parameters ---------- polyline : :class:`~compas.geometry.Polyline` A COMPAS polyline. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`. """ - def __init__(self, polyline, layer=None, **kwargs): - super(PolylineArtist, self).__init__(primitive=polyline, layer=layer, **kwargs) + def __init__(self, polyline, **kwargs): + super(PolylineArtist, self).__init__(geometry=polyline, **kwargs) - def draw(self, color=None, show_points=False): + def draw(self, color=None): """Draw the polyline. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the polyline. - Default is :attr:`compas.artists.PrimitiveArtist.color`. - show_points : bool, optional - If True, draw the points of the polyline. + + Returns + ------- + System.Guid + The GUID of the created Rhino object. + + """ + color = Color.coerce(color) or self.color + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + + return sc.doc.Objects.AddPolyline(polyline_to_rhino(self.geometry), attr) + + def draw_points(self, color=None): + """Draw the polyline points. + + Parameters + ---------- + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional + The RGB color of the polyline points. Returns ------- @@ -44,12 +61,12 @@ def draw(self, color=None, show_points=False): """ color = Color.coerce(color) or self.color - color = color.rgb255 - _points = map(list, self.primitive.points) + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + guids = [] - if show_points: - points = [{"pos": point, "color": 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": color, "name": self.primitive.name}] - guids += compas_rhino.draw_polylines(polylines, layer=self.layer, clear=False, redraw=False) + + for point in self.geometry.points: + guid = sc.doc.Objects.AddPoint(point_to_rhino(point), attr) + guids.append(guid) + return guids diff --git a/src/compas_rhino/artists/robotmodelartist.py b/src/compas_rhino/artists/robotmodelartist.py index 8f2f1c7d514..a36cd4ca4ee 100644 --- a/src/compas_rhino/artists/robotmodelartist.py +++ b/src/compas_rhino/artists/robotmodelartist.py @@ -4,40 +4,38 @@ import time -import Rhino.Geometry -import scriptcontext as sc -from System.Drawing import Color -from Rhino.DocObjects.ObjectColorSource import ColorFromObject -from Rhino.DocObjects.ObjectColorSource import ColorFromLayer -from Rhino.DocObjects.ObjectMaterialSource import MaterialFromObject +import Rhino.Geometry # type: ignore +import scriptcontext as sc # type: ignore +from System.Drawing import Color # type: ignore +from Rhino.DocObjects.ObjectColorSource import ColorFromObject # type: ignore +from Rhino.DocObjects.ObjectColorSource import ColorFromLayer # type: ignore +from Rhino.DocObjects.ObjectMaterialSource import MaterialFromObject # type: ignore -from compas.artists import RobotModelArtist +from compas.artists import RobotModelArtist as BaseArtist import compas_rhino from compas_rhino.artists import RhinoArtist -from compas_rhino.geometry.transformations import xform_from_transformation +from compas_rhino.conversions import transformation_to_rhino -class RobotModelArtist(RhinoArtist, RobotModelArtist): +class RobotModelArtist(RhinoArtist, BaseArtist): """Artist for drawing robot models. Parameters ---------- model : :class:`~compas.robots.RobotModel` Robot model. - layer : str, optional - The name of the layer that will contain the robot meshes. **kwargs : dict, optional Additional keyword arguments. For more info, see :class:`RhinoArtist` and :class:`RobotModelArtist`. """ - def __init__(self, model, layer=None, **kwargs): - super(RobotModelArtist, self).__init__(model=model, layer=layer, **kwargs) + def __init__(self, model, **kwargs): + super(RobotModelArtist, self).__init__(model=model, **kwargs) def transform(self, native_mesh, transformation): - T = xform_from_transformation(transformation) + T = transformation_to_rhino(transformation) native_mesh.Transform(T) def create_geometry(self, geometry, name=None, color=None): diff --git a/src/compas_rhino/artists/sphereartist.py b/src/compas_rhino/artists/sphereartist.py index 2f60e5d368f..58c8891dd08 100644 --- a/src/compas_rhino/artists/sphereartist.py +++ b/src/compas_rhino/artists/sphereartist.py @@ -2,62 +2,44 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import ShapeArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import sphere_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class SphereArtist(RhinoArtist, ShapeArtist): +class SphereArtist(RhinoArtist, GeometryArtist): """Artist for drawing sphere shapes. Parameters ---------- sphere : :class:`~compas.geometry.Sphere` A COMPAS sphere. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`. """ - def __init__(self, sphere, layer=None, **kwargs): - super(SphereArtist, self).__init__(shape=sphere, layer=layer, **kwargs) + def __init__(self, sphere, **kwargs): + super(SphereArtist, self).__init__(geometry=sphere, **kwargs) - def draw(self, color=None, u=None, v=None): + def draw(self, color=None): """Draw the sphere associated with the artist. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the sphere. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`SphereArtist.u`. - v : int, optional - Number of faces in the "v" direction. - Default is :attr:`SphereArtist.v`. Returns ------- - list[System.Guid] - The GUIDs of the objects created in Rhino. + System.Guid + The GUID of the object created in Rhino. """ color = Color.coerce(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] - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=self.layer, - name=self.shape.name, - color=color.rgb255, - disjoint=True, - ) - return [guid] + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + return sc.doc.Objects.AddSphere(sphere_to_rhino(self.geometry), attr) diff --git a/src/compas_rhino/artists/surfaceartist.py b/src/compas_rhino/artists/surfaceartist.py index f267331f59f..6572d4b06a5 100644 --- a/src/compas_rhino/artists/surfaceartist.py +++ b/src/compas_rhino/artists/surfaceartist.py @@ -2,45 +2,45 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import SurfaceArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import surface_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class SurfaceArtist(RhinoArtist, SurfaceArtist): +class SurfaceArtist(RhinoArtist, GeometryArtist): """Artist for drawing surfaces. Parameters ---------- - surface : :class:`~compas.geometry.Surface` + surface : :class:`~compas.geometry.Geometry` A COMPAS surface. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`~compas.artists.SurfaceArtist`. """ - def __init__(self, surface, layer=None, **kwargs): - super(SurfaceArtist, self).__init__(surface=surface, layer=layer, **kwargs) + def __init__(self, surface, **kwargs): + super(SurfaceArtist, self).__init__(geometry=surface, **kwargs) def draw(self, color=None): """Draw the surface. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the surface. - The default color is :attr:`compas.artists.SurfaceArtist.color`. Returns ------- - list[System.Guid] - The GUIDs of the created Rhino objects. + System.Guid + The GUID of the created Rhino object. """ color = Color.coerce(color) or self.color - surfaces = [{"surface": self.surface, "color": color.rgb255, "name": self.surface.name}] - return compas_rhino.draw_surfaces(surfaces, layer=self.layer, clear=False, redraw=False) + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + surface = surface_to_rhino(self.geometry) + return sc.doc.Objects.AddSurface(surface, attr) diff --git a/src/compas_rhino/artists/torusartist.py b/src/compas_rhino/artists/torusartist.py index 6d73c6a6395..53247b91135 100644 --- a/src/compas_rhino/artists/torusartist.py +++ b/src/compas_rhino/artists/torusartist.py @@ -2,62 +2,45 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino -from compas.artists import ShapeArtist +import scriptcontext as sc # type: ignore + +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import torus_to_rhino_brep from .artist import RhinoArtist +from ._helpers import attributes -class TorusArtist(RhinoArtist, ShapeArtist): +class TorusArtist(RhinoArtist, GeometryArtist): """Artist for drawing torus shapes. Parameters ---------- torus : :class:`~compas.geometry.Torus` A COMPAS torus. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`ShapeArtist`. """ - def __init__(self, torus, layer=None, **kwargs): - super(TorusArtist, self).__init__(shape=torus, layer=layer, **kwargs) + def __init__(self, torus, **kwargs): + super(TorusArtist, self).__init__(geometry=torus, **kwargs) - def draw(self, color=None, u=None, v=None): + def draw(self, color=None): """Draw the torus associated with the artist. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the torus. - Default is :attr:`compas.artists.ShapeArtist.color`. - u : int, optional - Number of faces in the "u" direction. - Default is :attr:`TorusArtist.u`. - v : int, optional - Number of faces in the "v" direction. - Default is :attr:`TorusArtist.v`. Returns ------- - list[System.Guid] - The GUIDs of the objects created in Rhino. + System.Guid + The GUID of the object created in Rhino. """ color = Color.coerce(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] - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=self.layer, - name=self.shape.name, - color=color.rgb255, - disjoint=True, - ) - return [guid] + attr = attributes(name=self.geometry.name, color=color, layer=self.layer) + brep = torus_to_rhino_brep(self.geometry) + return sc.doc.Objects.AddBrep(brep, attr) diff --git a/src/compas_rhino/artists/vectorartist.py b/src/compas_rhino/artists/vectorartist.py index 938741b84b6..484a41820fa 100644 --- a/src/compas_rhino/artists/vectorartist.py +++ b/src/compas_rhino/artists/vectorartist.py @@ -2,70 +2,53 @@ from __future__ import absolute_import from __future__ import division -import compas_rhino +import scriptcontext as sc # type: ignore + from compas.geometry import Point -from compas.artists import PrimitiveArtist +from compas.artists import GeometryArtist from compas.colors import Color +from compas_rhino.conversions import point_to_rhino from .artist import RhinoArtist +from ._helpers import attributes -class VectorArtist(RhinoArtist, PrimitiveArtist): +class VectorArtist(RhinoArtist, GeometryArtist): """Artist for drawing vectors. Parameters ---------- vector : :class:`~compas.geometry.Vector` A COMPAS vector. - layer : str, optional - The layer that should contain the drawing. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`PrimitiveArtist`. """ - def __init__(self, vector, layer=None, **kwargs): - super(VectorArtist, self).__init__(primitive=vector, layer=layer, **kwargs) + def __init__(self, vector, **kwargs): + super(VectorArtist, self).__init__(geometry=vector, **kwargs) - def draw(self, color=None, point=None, show_point=False): + def draw(self, color=None, point=None): """Draw the vector. Parameters ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`~compas.colors.Color`, optional + color : rgb1 | rgb255 | :class:`~compas.colors.Color`, optional The RGB color of the vector. - Default is :attr:`compas.artists.PrimitiveArtist.color`. point : [float, float, float] | :class:`~compas.geometry.Point`, optional Point of application of the vector. Default is ``Point(0, 0, 0)``. - show_point : bool, optional - If True, draw the base point of the vector. Returns ------- - list[System.Guid] - The GUIDs of the created Rhino objects. + System.Guid + The GUID of the created Rhino object. """ color = Color.coerce(color) or self.color - color = color.rgb255 + attr = attributes(name=self.geometry.name, color=color, layer=self.layer, arrow="end") + point = point or [0, 0, 0] start = Point(*point) - end = start + self.primitive - start = list(start) - end = list(end) - guids = [] - if show_point: - points = [{"pos": start, "color": 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": color, - "name": self.primitive.name, - } - ] - guids += compas_rhino.draw_lines(lines, layer=self.layer, clear=False, redraw=False) - return guids + end = start + self.geometry + + return sc.doc.Objects.AddLine(point_to_rhino(start), point_to_rhino(end), attr) diff --git a/src/compas_rhino/artists/volmeshartist.py b/src/compas_rhino/artists/volmeshartist.py index 93e137b5196..6ebc9aca6cc 100644 --- a/src/compas_rhino/artists/volmeshartist.py +++ b/src/compas_rhino/artists/volmeshartist.py @@ -2,29 +2,35 @@ from __future__ import absolute_import from __future__ import division +from Rhino.Geometry import TextDot # type: ignore +import scriptcontext as sc # type: ignore + import compas_rhino from compas.geometry import centroid_points -from compas.artists import VolMeshArtist +from compas.geometry import Line +from compas.artists import VolMeshArtist as BaseArtist +from compas_rhino.conversions import point_to_rhino +from compas_rhino.conversions import line_to_rhino +from compas_rhino.conversions import vertices_and_faces_to_rhino from .artist import RhinoArtist +from ._helpers import attributes +from ._helpers import ngon -class VolMeshArtist(RhinoArtist, VolMeshArtist): +class VolMeshArtist(RhinoArtist, BaseArtist): """Artist for drawing volmesh data structures. Parameters ---------- volmesh : :class:`~compas.datastructures.VolMesh` A COMPAS volmesh. - layer : str, optional - The name of the layer that will contain the volmesh. **kwargs : dict, optional Additional keyword arguments. - For more info, see :class:`RhinoArtist` and :class:`VolMeshArtist`. """ - def __init__(self, volmesh, layer=None, **kwargs): - super(VolMeshArtist, self).__init__(volmesh=volmesh, layer=layer, **kwargs) + def __init__(self, volmesh, **kwargs): + super(VolMeshArtist, self).__init__(volmesh=volmesh, **kwargs) # ========================================================================== # clear @@ -38,7 +44,7 @@ def clear(self): None """ - guids = compas_rhino.get_objects(name="{}.*".format(self.volmesh.name)) + guids = compas_rhino.get_objects(name="{}.*".format(self.volmesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_vertices(self): @@ -49,7 +55,7 @@ def clear_vertices(self): None """ - guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.volmesh.name)) + guids = compas_rhino.get_objects(name="{}.vertex.*".format(self.volmesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_edges(self): @@ -60,7 +66,7 @@ def clear_edges(self): None """ - guids = compas_rhino.get_objects(name="{}.edge.*".format(self.volmesh.name)) + guids = compas_rhino.get_objects(name="{}.edge.*".format(self.volmesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_faces(self): @@ -71,7 +77,7 @@ def clear_faces(self): None """ - guids = compas_rhino.get_objects(name="{}.face.*".format(self.volmesh.name)) + guids = compas_rhino.get_objects(name="{}.face.*".format(self.volmesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_cells(self): @@ -82,7 +88,7 @@ def clear_cells(self): None """ - guids = compas_rhino.get_objects(name="{}.cell.*".format(self.volmesh.name)) + guids = compas_rhino.get_objects(name="{}.cell.*".format(self.volmesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_vertexlabels(self): @@ -93,7 +99,7 @@ def clear_vertexlabels(self): None """ - guids = compas_rhino.get_objects(name="{}.vertexlabel.*".format(self.volmesh.name)) + guids = compas_rhino.get_objects(name="{}.vertex.*.label".format(self.volmesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_edgelabels(self): @@ -104,7 +110,7 @@ def clear_edgelabels(self): None """ - guids = compas_rhino.get_objects(name="{}.edgelabel.*".format(self.volmesh.name)) + guids = compas_rhino.get_objects(name="{}.edge.*.label".format(self.volmesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_facelabels(self): @@ -115,7 +121,7 @@ def clear_facelabels(self): None """ - guids = compas_rhino.get_objects(name="{}.facelabel.*".format(self.volmesh.name)) + guids = compas_rhino.get_objects(name="{}.face.*.label".format(self.volmesh.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) # ========================================================================== @@ -143,7 +149,7 @@ def draw(self, cells=None, color=None): """ return self.draw_cells(cells=cells, color=color) - def draw_vertices(self, vertices=None, color=None): + def draw_vertices(self, vertices=None, color=None, group=None): """Draw a selection of vertices. Parameters @@ -153,29 +159,32 @@ def draw_vertices(self, vertices=None, color=None): Default is None, in which case all vertices are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the vertices. - The default color of the vertices is :attr:`VolMeshArtist.default_vertexcolor`. + group : str, optional + The name of the group in which the vertices are combined. Returns ------- list[System.Guid] - The GUIDs of the created Rhino objects. + The GUIDs of the created Rhino point objects. """ self.vertex_color = color - vertices = vertices or self.vertices - vertex_xyz = self.vertex_xyz - points = [] - for vertex in vertices: - points.append( - { - "pos": vertex_xyz[vertex], - "name": "{}.vertex.{}".format(self.volmesh.name, vertex), - "color": self.vertex_color[vertex].rgb255, - } - ) - return compas_rhino.draw_points(points, layer=self.layer, clear=False, redraw=False) - - def draw_edges(self, edges=None, color=None): + + guids = [] + + for vertex in vertices or self.volmesh.vertices(): # type: ignore + name = "{}.vertex.{}".format(self.volmesh.name, vertex) # type: ignore + color = self.vertex_color[vertex] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + point = self.vertex_xyz[vertex] + + guid = sc.doc.Objects.AddPoint(point_to_rhino(point), attr) + guids.append(guid) + + return guids + + def draw_edges(self, edges=None, color=None, group=None): """Draw a selection of edges. Parameters @@ -185,30 +194,35 @@ def draw_edges(self, edges=None, color=None): The default is None, in which case all edges are drawn. color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional The color of the edges. - The default color is :attr:`VolMeshArtist.default_edgecolor`. + group : str, optional + The name of the group in which the edges are combined. Returns ------- list[System.Guid] - The GUIDs of the created Rhino objects. + The GUIDs of the created Rhino line 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[edge].rgb255, - "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): + + guids = [] + + for edge in edges or self.volmesh.edges(): # type: ignore + name = "{}.edge.{}-{}".format(self.volmesh.name, *edge) # type: ignore + color = self.edge_color[edge] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + line = Line(self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]) + + guid = sc.doc.Objects.AddLine(line_to_rhino(line), attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_faces(self, faces=None, color=None, group=None): """Draw a selection of faces. Parameters @@ -218,7 +232,8 @@ def draw_faces(self, faces=None, color=None): The default is None, in which case all faces are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the faces. - The default color is :attr:`VolMeshArtist.default_facecolor`. + group : str, optional + The name of the group in which the faces are combined. Returns ------- @@ -227,20 +242,27 @@ def draw_faces(self, faces=None, color=None): """ self.face_color = color - faces = faces or self.faces - vertex_xyz = self.vertex_xyz - 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": self.face_color[face].rgb255, - } - ) - return compas_rhino.draw_faces(facets, layer=self.layer, clear=False, redraw=False) - - def draw_cells(self, cells=None, color=None): + + guids = [] + + for face in faces or self.volmesh.faces(): # type: ignore + name = "{}.face.{}".format(self.volmesh.name, face) # type: ignore + color = self.face_color[face] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + vertices = [self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)] # type: ignore + facet = ngon(vertices) + + if facet: + guid = sc.doc.Objects.AddMesh(vertices_and_faces_to_rhino(vertices, [facet]), attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_cells(self, cells=None, color=None, group=None): """Draw a selection of cells. Parameters @@ -250,7 +272,8 @@ def draw_cells(self, cells=None, color=None): The default is None, in which case all cells are drawn. color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional The color of the cells. - The default color is :attr:`VolMeshArtist.default_cellcolor`. + group : str, optional + The name of the group in which the cells are combined. Returns ------- @@ -260,38 +283,44 @@ def draw_cells(self, cells=None, color=None): """ self.cell_color = color - cells = cells or self.cells - vertex_xyz = self.vertex_xyz + guids = [] - for cell in cells: - vertices = self.volmesh.cell_vertices(cell) - faces = self.volmesh.cell_faces(cell) + + for cell in cells or self.volmesh.cells(): # type: ignore + name = "{}.cell.{}".format(self.volmesh.name, cell) # type: ignore + color = self.cell_color[cell] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + vertices = self.volmesh.cell_vertices(cell) # type: ignore + faces = self.volmesh.cell_faces(cell) # type: ignore vertex_index = dict((vertex, index) for index, vertex in enumerate(vertices)) - vertices = [vertex_xyz[vertex] for vertex in vertices] - faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] - guid = compas_rhino.draw_mesh( - vertices, - faces, - layer=self.layer, - name="{}.cell.{}".format(self.volmesh.name, cell), - color=self.cell_color[cell].rgb255, - disjoint=True, - ) + vertices = [self.vertex_xyz[vertex] for vertex in vertices] + faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] # type: ignore + + guid = sc.doc.Objects.AddMesh(vertices_and_faces_to_rhino(vertices, faces, disjoint=True), attr) guids.append(guid) + return guids - # ========================================================================== + # ============================================================================= # draw labels - # ========================================================================== + # ============================================================================= - def draw_vertexlabels(self, text=None): - """Draw labels for a selection vertices. + def draw_vertexlabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): + """Draw a selection of vertex labels. Parameters ---------- - text : dict[int, str], optional + text : dict[int, str] A dictionary of vertex labels as vertex-text pairs. - The default value is None, in which case every vertex will be labelled with its key. + color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color of the vertices. + group : str, optional + The name of the group in which the labels are combined. + fontheight : int, optional + Font height of the vertex labels. + fontface : str, optional + Font face of the vertex labels. Returns ------- @@ -299,28 +328,44 @@ def draw_vertexlabels(self, text=None): The GUIDs of the created Rhino objects. """ - self.vertex_text = text - vertex_xyz = self.vertex_xyz - labels = [] - for vertex in self.vertex_text: - labels.append( - { - "pos": vertex_xyz[vertex], - "name": "{}.vertexlabel.{}".format(self.volmesh.name, vertex), - "color": self.vertex_color[vertex].rgb255, - "text": self.vertex_text[vertex], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) - - def draw_edgelabels(self, text=None): - """Draw labels for a selection of edges. + self.vertex_color = color + + guids = [] + + for vertex in text: + name = "{}.vertex.{}.label".format(self.volmesh.name, vertex) # type: ignore + color = self.vertex_color[vertex] # type: ignore + attr = attributes(name=name, color=color, layer=self.layer) + + point = self.vertex_xyz[vertex] + + dot = TextDot(str(text[vertex]), point) # type: ignore + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_edgelabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): + """Draw a selection of edge labels. Parameters ---------- text : dict[tuple[int, int], str], 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. + color : :class:`~compas.colors.Color` | dict[tuple[int, int], :class:`~compas.colors.Color`], optional + The color of the edges. + group : str, optional + The name of the group in which the labels are combined. + fontheight : int, optional + Font height of the edge labels. + fontface : str, optional + Font face of the edge labels. Returns ------- @@ -328,28 +373,45 @@ def draw_edgelabels(self, text=None): The GUIDs of the created Rhino objects. """ - self.edge_text = text - vertex_xyz = self.vertex_xyz - labels = [] - for edge in self.edge_text: - labels.append( - { - "pos": centroid_points([vertex_xyz[edge[0]], vertex_xyz[edge[1]]]), - "name": "{}.edgelabel.{}-{}".format(self.volmesh.name, *edge), - "color": self.edge_color[edge].rgb255, - "text": self.edge_text[edge], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) - - def draw_facelabels(self, text=None): - """Draw labels for a selection of faces. + self.edge_color = color + + guids = [] + + for edge in text: + name = "{}.edge.{}-{}.label".format(self.volmesh.name, *edge) # type: ignore + color = self.edge_color[edge] # type: ignore + attr = attributes(name="{}.label".format(name), color=color, layer=self.layer) + + line = Line(self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]) + point = point_to_rhino(line.midpoint) + + dot = TextDot(str(text[edge]), point) # type: ignore + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_facelabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): + """Draw a selection of face labels. Parameters ---------- - text : dict[int, str], optional + text : dict[int, str] 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 : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color of the faces. + group : str, optional + The name of the group in which the labels are combined. + fontheight : int, optional + Font height of the face labels. + fontface : str, optional + Font face of the face labels. Returns ------- @@ -357,28 +419,45 @@ def draw_facelabels(self, text=None): The GUIDs of the created Rhino objects. """ - self.face_text = text - vertex_xyz = self.vertex_xyz - labels = [] - for face in self.face_text: - labels.append( - { - "pos": centroid_points([vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)]), - "name": "{}.facelabel.{}".format(self.volmesh.name, face), - "color": self.face_color[face].rgb255, - "text": self.face_text[face], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) - - def draw_celllabels(self, text=None): - """Draw labels for cells. + self.face_color = color + + guids = [] + + for face in text: + name = "{}.face.{}.label".format(self.volmesh.name, face) # type: ignore + color = self.face_color[face] # type: ignore + attr = attributes(name="{}.label".format(name), color=color, layer=self.layer) + + vertices = [self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)] # type: ignore + point = point_to_rhino(centroid_points(vertices)) # type: ignore + + dot = TextDot(str(text[face]), point) # type: ignore + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + def draw_celllabels(self, text, color=None, group=None, fontheight=10, fontface="Arial Regular"): + """Draw a selection of cells. Parameters ---------- text : dict[int, str], optional - A dictionary of cell labels as cell-text pairs. - The default value is None, in which case every cell will be labelled with its key. + A dictionary of face labels as cell-text pairs. + color : :class:`~compas.colors.Color` | dict[int, :class:`~compas.colors.Color`], optional + The color of the cells. + group : str, optional + The name of the group in which the labels are combined. + fontheight : int, optional + Font height of the cell labels. + fontface : str, optional + Font face of the cell labels. Returns ------- @@ -386,16 +465,34 @@ def draw_celllabels(self, text=None): The GUIDs of the created Rhino objects. """ - self.cell_text = text - vertex_xyz = self.vertex_xyz - labels = [] - for cell in self.cell_text: - labels.append( - { - "pos": centroid_points([vertex_xyz[vertex] for vertex in self.volmesh.cell_vertices(cell)]), - "name": "{}.facelabel.{}".format(self.volmesh.name, cell), - "color": self.cell_color[cell].rgb255, - "text": self.cell_text[cell], - } - ) - return compas_rhino.draw_labels(labels, layer=self.layer, clear=False, redraw=False) + self.cell_color = color + + guids = [] + + for cell in text: + name = "{}.cell.{}.label".format(self.volmesh.name, cell) # type: ignore + color = self.cell_color[cell] # type: ignore + attr = attributes(name="{}.label".format(name), color=color, layer=self.layer) + + vertices = [self.vertex_xyz[vertex] for vertex in self.volmesh.cell_vertices(cell)] # type: ignore + point = point_to_rhino(centroid_points(vertices)) # type: ignore + + dot = TextDot(str(text[cell]), point) # type: ignore + dot.FontHeight = fontheight + dot.FontFace = fontface + + guid = sc.doc.Objects.AddTextDot(dot, attr) + guids.append(guid) + + if group: + self.add_to_group(group, guids) + + return guids + + # ============================================================================= + # draw normals + # ============================================================================= + + # ============================================================================= + # draw miscellaneous + # ============================================================================= diff --git a/src/compas_rhino/conversions/__init__.py b/src/compas_rhino/conversions/__init__.py index b50bee12d6f..5cacdd53149 100644 --- a/src/compas_rhino/conversions/__init__.py +++ b/src/compas_rhino/conversions/__init__.py @@ -9,30 +9,6 @@ Conversions between Rhino geometry objects (:mod:`Rhino.Geometry`) and COMPAS geometry objects (:mod:`compas.geometry`). -Classes -======= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - RhinoGeometry - RhinoBox - RhinoCircle - RhinoCone - RhinoCurve - RhinoCylinder - RhinoEllipse - RhinoLine - RhinoMesh - RhinoPlane - RhinoPoint - RhinoPolyline - RhinoSphere - RhinoSurface - RhinoVector - - Exceptions ========== @@ -43,220 +19,243 @@ ConversionError -Functions -========= - -Primitives ----------- +To Rhino +======== .. autosummary:: :toctree: generated/ :nosignatures: - point_to_rhino - vector_to_rhino - line_to_rhino - plane_to_rhino - frame_to_rhino - frame_to_rhino_plane + arc_to_rhino + box_to_rhino + brep_to_rhino + capsule_to_rhino_brep circle_to_rhino + circle_to_rhino_curve + cone_to_rhino + cone_to_rhino_brep + curve_to_rhino + cylinder_to_rhino + cylinder_to_rhino_brep ellipse_to_rhino - polyline_to_rhino + ellipse_to_rhino_curve + frame_to_rhino + frame_to_rhino_plane + line_to_rhino + line_to_rhino_curve + mesh_to_rhino + plane_to_rhino + point_to_rhino polygon_to_rhino - arc_to_rhino - point_to_compas - vector_to_compas - line_to_compas - plane_to_compas - plane_to_compas_frame - circle_to_compas - ellipse_to_compas - polyline_to_compas - polygon_to_compas - arc_to_compas + polyhedron_to_rhino + polyline_to_rhino + polyline_to_rhino_curve + sphere_to_rhino + surface_to_rhino + torus_to_rhino + torus_to_rhino_brep + transformation_to_rhino + transformation_matrix_to_rhino + vertices_and_faces_to_rhino + vector_to_rhino -Shapes ------- +To COMPAS +========= .. autosummary:: :toctree: generated/ :nosignatures: - box_to_rhino - sphere_to_rhino - cone_to_rhino - cylinder_to_rhino + arc_to_compas box_to_compas - sphere_to_compas + brep_to_compas_box + brep_to_compas_cone + brep_to_compas_cylinder + brep_to_compas_sphere + circle_to_compas cone_to_compas - cylinder_to_compas - - -Curves ------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - line_to_rhino_curve - circle_to_rhino_curve - ellipse_to_rhino_curve - curve_to_compas_line curve_to_compas_circle curve_to_compas_ellipse + curve_to_compas_line curve_to_compas_polyline - - -Surfaces --------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - -Transformations ---------------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - xform_to_rhino + cylinder_to_compas + ellipse_to_compas + extrusion_to_compas_box + extrusion_to_compas_cylinder + extrusion_to_compas_torus + line_to_compas + mesh_to_compas + plane_to_compas + plane_to_compas_frame + point_to_compas + polygon_to_compas + polyline_to_compas + sphere_to_compas + surface_to_compas + surface_to_compas_data + surface_to_compas_mesh + surface_to_compas_quadmesh + vector_to_compas """ from __future__ import absolute_import -from ._exceptions import ConversionError +from .exceptions import ConversionError -from ._primitives import ( +from .geometry import ( point_to_rhino, vector_to_rhino, - line_to_rhino, plane_to_rhino, frame_to_rhino, frame_to_rhino_plane, - circle_to_rhino, - ellipse_to_rhino, - polyline_to_rhino, polygon_to_rhino, - arc_to_rhino, point_to_compas, vector_to_compas, - line_to_compas, plane_to_compas, plane_to_compas_frame, + polygon_to_compas, +) +from .curves import ( + arc_to_rhino, + circle_to_rhino, + circle_to_rhino_curve, + curve_to_rhino, + ellipse_to_rhino, + ellipse_to_rhino_curve, + line_to_rhino, + line_to_rhino_curve, + polyline_to_rhino, + polyline_to_rhino_curve, + arc_to_compas, circle_to_compas, + curve_to_compas_circle, + curve_to_compas_ellipse, + curve_to_compas_line, + curve_to_compas_polyline, ellipse_to_compas, + line_to_compas, polyline_to_compas, - polygon_to_compas, - arc_to_compas, ) -from ._shapes import ( +from .surfaces import ( + surface_to_rhino, + data_to_rhino_surface, + surface_to_compas_data, + surface_to_compas, + surface_to_compas_mesh, + surface_to_compas_quadmesh, +) +from .shapes import ( box_to_rhino, sphere_to_rhino, + capsule_to_rhino_brep, cone_to_rhino, + cone_to_rhino_brep, cylinder_to_rhino, + cylinder_to_rhino_brep, + torus_to_rhino, + torus_to_rhino_brep, box_to_compas, sphere_to_compas, cone_to_compas, cylinder_to_compas, ) -from ._curves import ( - line_to_rhino_curve, - circle_to_rhino_curve, - ellipse_to_rhino_curve, - curve_to_compas_circle, - curve_to_compas_ellipse, - curve_to_compas_line, - curve_to_compas_polyline, -) - -from ._meshes import ( +from .meshes import ( mesh_to_rhino, - polyhedron_to_rhino, vertices_and_faces_to_rhino, + mesh_to_compas, + polyhedron_to_rhino, +) +from .breps import ( + brep_to_rhino, + brep_to_compas_box, + brep_to_compas_cone, + brep_to_compas_cylinder, + brep_to_compas_sphere, +) +from .extrusions import ( + extrusion_to_compas_box, + extrusion_to_compas_cylinder, + extrusion_to_compas_torus, ) -from ._transformations import xform_to_rhino - -from ._geometry import RhinoGeometry - -from .box import RhinoBox -from .circle import RhinoCircle -from .cone import RhinoCone -from .curve import RhinoCurve -from .cylinder import RhinoCylinder -from .ellipse import RhinoEllipse -from .line import RhinoLine -from .mesh import RhinoMesh -from .plane import RhinoPlane -from .point import RhinoPoint -from .polyline import RhinoPolyline -from .sphere import RhinoSphere -from .surface import RhinoSurface -from .vector import RhinoVector - -BaseRhinoGeometry = RhinoGeometry +from .transformations import ( + transformation_to_rhino, + transformation_matrix_to_rhino, +) __all__ = [ "ConversionError", + # geometry "point_to_rhino", "vector_to_rhino", - "line_to_rhino", "plane_to_rhino", "frame_to_rhino", "frame_to_rhino_plane", - "circle_to_rhino", - "ellipse_to_rhino", - "polyline_to_rhino", "polygon_to_rhino", "point_to_compas", "vector_to_compas", - "line_to_compas", "plane_to_compas", "plane_to_compas_frame", + "polygon_to_compas", + # curves + "line_to_rhino", + "line_to_rhino_curve", + "polyline_to_rhino", + "polyline_to_rhino_curve", + "circle_to_rhino", + "circle_to_rhino_curve", + "ellipse_to_rhino", + "ellipse_to_rhino_curve", + "arc_to_rhino", + "curve_to_rhino", + "line_to_compas", + "polyline_to_compas", "circle_to_compas", "ellipse_to_compas", - "polyline_to_compas", - "polygon_to_compas", + "arc_to_compas", + "curve_to_compas_circle", + "curve_to_compas_ellipse", + "curve_to_compas_line", + "curve_to_compas_polyline", + # surfaces + "surface_to_rhino", + "surface_to_compas_data", + "data_to_rhino_surface", + "surface_to_compas", + "surface_to_compas_mesh", + "surface_to_compas_quadmesh", + # shapes "box_to_rhino", "sphere_to_rhino", + "capsule_to_rhino_brep", "cone_to_rhino", + "cone_to_rhino_brep", "cylinder_to_rhino", - "arc_to_rhino", - "arc_to_compas", + "cylinder_to_rhino_brep", + "torus_to_rhino", + "torus_to_rhino_brep", "box_to_compas", "sphere_to_compas", "cone_to_compas", "cylinder_to_compas", - "line_to_rhino_curve", - "circle_to_rhino_curve", - "ellipse_to_rhino_curve", - "curve_to_compas_circle", - "curve_to_compas_ellipse", - "curve_to_compas_line", - "curve_to_compas_polyline", + # meshes "mesh_to_rhino", "polyhedron_to_rhino", "vertices_and_faces_to_rhino", - "xform_to_rhino", - "RhinoGeometry", - "RhinoBox", - "RhinoCircle", - "RhinoCone", - "RhinoCurve", - "RhinoCylinder", - "RhinoEllipse", - "RhinoLine", - "RhinoMesh", - "RhinoPlane", - "RhinoPoint", - "RhinoPolyline", - "RhinoSphere", - "RhinoSurface", - "RhinoVector", + "mesh_to_compas", + # breps + "brep_to_rhino", + "brep_to_compas_box", + "brep_to_compas_cone", + "brep_to_compas_cylinder", + "brep_to_compas_sphere", + # extrusions + "extrusion_to_compas_box", + "extrusion_to_compas_cylinder", + "extrusion_to_compas_torus", + # transformations + "transformation_to_rhino", + "transformation_matrix_to_rhino", ] diff --git a/src/compas_rhino/conversions/_curves.py b/src/compas_rhino/conversions/_curves.py deleted file mode 100644 index 6fffc0da010..00000000000 --- a/src/compas_rhino/conversions/_curves.py +++ /dev/null @@ -1,212 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.geometry import Line - -from ._exceptions import ConversionError - -from ._primitives import line_to_rhino -from ._primitives import circle_to_rhino -from ._primitives import ellipse_to_rhino -from ._primitives import point_to_compas -from ._primitives import circle_to_compas -from ._primitives import ellipse_to_compas -from ._primitives import polyline_to_compas - -from Rhino.Geometry import NurbsCurve as RhinoNurbsCurve - - -def curve_to_compas_line(curve): - """Convert a Rhino curve to a COMPAS line. - - Parameters - ---------- - curve: :rhino:`Rhino.Geometry.Curve` - - Returns - ------- - :class:`~compas.geometry.Line` - - """ - return Line(point_to_compas(curve.PointAtStart), point_to_compas(curve.PointAtEnd)) - - -def line_to_rhino_curve(line): - """Convert a COMPAS line to a Rhino curve. - - Parameters - ---------- - line: :class:`~compas.geometry.Line` - - Returns - ------- - :rhino:`Rhino.Geometry.Curve` - - """ - return RhinoNurbsCurve.CreateFromLine(line_to_rhino(line)) - - -def curve_to_compas_circle(curve): - """Convert a Rhino curve to a COMPAS circle. - - Parameters - ---------- - curve: :rhino:`Rhino.Geometry.Curve` - - Returns - ------- - :class:`~compas.geometry.Circle` - - Raises - ------ - ConversionError - If the curve cannot be converted to a circle. - - """ - result, circle = curve.TryGetCircle() - if not result: - raise ConversionError("The curve cannot be converted to a circle.") - return circle_to_compas(circle) - - -def circle_to_rhino_curve(circle): - """Convert a COMPAS circle to a Rhino curve. - - Parameters - ---------- - circle: :class:`~compas.geometry.Circle` - - Returns - ------- - :rhino:`Rhino.Geometry.Curve` - - """ - return RhinoNurbsCurve.CreateFromCircle(circle_to_rhino(circle)) - - -def curve_to_compas_ellipse(curve): - """Convert a Rhino curve to a COMPAS ellipse. - - Parameters - ---------- - curve: :rhino:`Rhino.Geometry.Curve` - - Returns - ------- - :class:`~compas.geometry.Ellipse` - - Raises - ------ - ConversionError - If the curve cannot be converted to an ellipse. - - """ - result, ellipse = curve.TryGetEllipse() - if not result: - raise ConversionError("The curve cannot be converted to an ellipse.") - return ellipse_to_compas(ellipse) - - -def ellipse_to_rhino_curve(ellipse): - """Convert a COMPAS ellipse to a Rhino curve. - - Parameters - ---------- - ellipse: :class:`~compas.geometry.Ellipse` - - Returns - ------- - :rhino:`Rhino.Geometry.Curve` - - """ - return RhinoNurbsCurve.CreateFromEllipse(ellipse_to_rhino(ellipse)) - - -def curve_to_compas_polyline(curve): - """Convert a Rhino curve to a COMPAS polyline. - - Parameters - ---------- - curve: :rhino:`Rhino.Geometry.Curve` - - Returns - ------- - :class:`~compas.geometry.Polyline` - - Raises - ------ - ConversionError - If the curve cannot be converted to a polyline. - - """ - result, polyline = curve.TryGetPolyline() - if not result: - raise ConversionError("The curve cannot be converted to a polyline.") - return polyline_to_compas(polyline) - - -def curve_to_compas_data(curve): - """Convert a Rhino curve to a COMPAS data dict. - - Parameters - ---------- - curve: :rhino:`Rhino.Geometry.Curve` - - Returns - ------- - dict - - """ - nurbs = curve.ToNurbsCurve() - points = [] - weights = [] - knots = [] - multiplicities = [] - degree = nurbs.Degree - is_periodic = nurbs.IsPeriodic - - for index in range(nurbs.Points.Count): - point = nurbs.Points.Item[index] - points.append(point_to_compas(point.Location)) - weights.append(point.Weight) - - for index in range(nurbs.Knots.Count): - knots.append(nurbs.Knots.Item[index]) - multiplicities.append(nurbs.Knots.KnotMultiplicity(index)) - - return { - "points": [point.data for point in points], - "weights": weights, - "knots": knots, - "multiplicities": multiplicities, - "degree": degree, - "is_periodic": is_periodic, - } - - -def data_to_rhino_curve(data): - """Convert a COMPAS curve to a Rhino curve. - - Parameters - ---------- - data: dict - - Returns - ------- - :rhino:`Rhino.Geometry.NurbsCurve` - - """ - nurbs = RhinoNurbsCurve(data["degree"], len(data["points"])) - - for index, xyz in enumerate(data["points"]): - nurbs.Points.SetPoint(index, *xyz) - - knotvector = [] - for knot, mult in zip(data["knots"], data["multiplicities"]): - for i in range(mult): - knotvector.append(knot) - - for index, knot in enumerate(knotvector): - nurbs.Knots.Item[index] = knot - return nurbs diff --git a/src/compas_rhino/conversions/_geometry.py b/src/compas_rhino/conversions/_geometry.py deleted file mode 100644 index 86c9cf3bbc6..00000000000 --- a/src/compas_rhino/conversions/_geometry.py +++ /dev/null @@ -1,183 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino -import compas_rhino - - -class RhinoGeometry(object): - """Base class for Rhino Geometry and DocObject wrappers. - - Attributes - ---------- - name: str - The name of the object. - type: str - The type of the object. - guid: str - The GUID of the object. - object : :rhino:`Rhino_DocObjects_RhinoObject` - A reference to the Rhino DocObject, if it exists. - geometry: :rhino:`Rhino_Geometry_GeometryBase` - A reference to the Rhino Geometry Object. - - """ - - def __init__(self): - super(RhinoGeometry, self).__init__() - self._guid = None - self._object = None - self._geometry = None - self._type = None - self._name = None - - @property - def guid(self): - return self._guid - - @guid.setter - def guid(self, guid): - self.object = compas_rhino.find_object(guid) - - @property - def object(self): - return self._object - - @object.setter - def object(self, obj): - self._guid = obj.Id - self._object = obj - self.geometry = obj.Geometry - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - raise NotImplementedError - - @property - def type(self): - if self.object: - return self.object.ObjectType - return self._type - - @type.setter - def type(self, value): - self._type = value - - @property - def name(self): - if self.object: - return self.object.Attributes.Name - return self._name - - @name.setter - def name(self, value): - if self.object: - self.object.Attributes.Name = value - self.object.CommitChanges() - else: - self._name = value - - @classmethod - def from_guid(cls, guid): - """Try to construct a RhinoGeometry wrapper from the GUID of an existing Rhino DocObject. - - Parameters - ---------- - guid : str - The GUID of the Rhino DocObject. - - Returns - ------- - :class:`~compas_rhino.geometry.RhinoGeometry` - The Rhino object wrapper. - - Raises - ------ - :class:`ConversionError` - If the geometry of the Rhino DocObject cannot be converted to the geometry type of the wrapper. - - """ - wrapper = cls() - wrapper.guid = guid - return wrapper - - @classmethod - def from_object(cls, obj): - """Construct a Rhino object wrapper from an existing Rhino object. - - Parameters - ---------- - obj : :rhino:`Rhino_DocObjects_RhinoObject` - The Rhino object. - - Returns - ------- - :class:`~compas_rhino.geometry.RhinoGeometry` - The Rhino object wrapper. - - Raises - ------ - :class:`ConversionError` - If the geometry of the Rhino DocObject cannot be converted to the geometry type of the wrapper. - - """ - wrapper = cls() - wrapper.object = obj - return wrapper - - @classmethod - def from_geometry(cls, geometry): - """Construct a Rhino object wrapper from an existing Rhino object. - - Parameters - ---------- - geometry : :rhino:`Rhino_DocObjects_RhinoObject` - The Rhino object. - - Returns - ------- - :class:`~compas_rhino.geometry.RhinoGeometry` - The Rhino object wrapper. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to the geometry type of the wrapper. - - """ - wrapper = cls() - wrapper.geometry = geometry - return wrapper - - def to_compas(self): - raise NotImplementedError - - def transform(self, T): - """Transform the Rhino object. - - Parameters - ---------- - T : :class:`~compas.geometry.Transformation` or :rhino:`Rhino.Geometry.Transform` - The transformation matrix. - - Returns - ------- - None - The Rhino object is transformed in place. - - """ - if not isinstance(T, Rhino.Geometry.Transform): - M = Rhino.Geometry.Transform(0.0) - for i in range(4): - for j in range(4): - M[i, j] = T[i, j] - else: - M = T - self.geometry.Transform(M) - if self.object: - self.object.CommitChanges() diff --git a/src/compas_rhino/conversions/_meshes.py b/src/compas_rhino/conversions/_meshes.py deleted file mode 100644 index bd94082484e..00000000000 --- a/src/compas_rhino/conversions/_meshes.py +++ /dev/null @@ -1,117 +0,0 @@ -from compas.geometry import centroid_polygon -from compas.utilities import pairwise - -from Rhino.Geometry import Mesh as RhinoMesh - -try: - # MeshNgon is not available in older versions of Rhino - from Rhino.Geometry import MeshNgon -except ImportError: - MeshNgon = False - - -def mesh_to_rhino(compas_mesh, disjoint=True, face_callback=None): - """Convert a COMPAS Mesh or a Polyhedron to a Rhino mesh object. - - Parameters - ---------- - compas_mesh : :class:`compas.datastructures.Mesh` | :class:`compas.geometry.Polyhedron` - A COMPAS Mesh or a Polyhedron. - disjoint : bool, optional - If ``True``, each face of the resulting mesh will be independently defined (have a copy of its own vertices). - face_callback : callable, optional - Called after each face is created with the face as an agrument, useful for custom post-processing. - - Returns - ------- - :class:`Rhino.Geometry.Mesh` - A Rhino mesh object. - - """ - vertices, faces = compas_mesh.to_vertices_and_faces() - return vertices_and_faces_to_rhino(vertices, faces, disjoint, face_callback) - - -polyhedron_to_rhino = mesh_to_rhino - - -def vertices_and_faces_to_rhino(vertices, faces, disjoint=True, face_callback=None): - """Convert COMPAS vertices and faces to a Rhino mesh object. - - Parameters - ---------- - vertices : list[[float, float, float] | :class:`~compas.geometry.Point`] - A list of point locations. - faces : list[list[int]] - A list of faces as lists of indices into `vertices`. - disjoint : bool, optional - If ``True``, each face of the resulting mesh will be independently defined (have a copy of its own vertices). - face_callback : callable, optional - Called after each face is created with the face as an agrument, useful for custom post-processing. - - Returns - ------- - :class:`Rhino.Geometry.Mesh` - A Rhino mesh object. - - """ - face_callback = face_callback or (lambda _: None) - mesh = RhinoMesh() - - if disjoint: - for face in faces: - f = len(face) - if f < 3: - continue # ignore degenerate faces - if f > 4: - if MeshNgon is None: - raise NotImplementedError("MeshNgons are not supported in this version of Rhino.") - points = [vertices[vertex] for vertex in face] - centroid = centroid_polygon(points) - indices = [] - for point in points: - x, y, z = point - indices.append(mesh.Vertices.Add(x, y, z)) - c = mesh.Vertices.Add(*centroid) - - facets = [] - for i, j in pairwise(indices + indices[:1]): - facets.append(mesh.Faces.AddFace(i, j, c)) - ngon = MeshNgon.Create(indices, facets) - mesh.Ngons.AddNgon(ngon) - else: - # triangle or quad faces - v_indices = [] - for v_index in face: - x, y, z = vertices[v_index] - v_indices.append(mesh.Vertices.Add(x, y, z)) - mesh.Faces.AddFace(*v_indices) - face_callback(face) - - else: - for (x, y, z) in vertices: - mesh.Vertices.Add(x, y, z) - - for face in faces: - f = len(face) - if f < 3: - continue # ignore degenerate faces - if f > 4: - if MeshNgon is None: - raise NotImplementedError("MeshNgons are not supported in this version of Rhino.") - - centroid = centroid_polygon([vertices[index] for index in face]) - c = mesh.Vertices.Add(*centroid) - facets = [] - for i, j in pairwise(face + face[:1]): - facets.append(mesh.Faces.AddFace(i, j, c)) - ngon = MeshNgon.Create(face, facets) - mesh.Ngons.AddNgon(ngon) - else: - # triangle or quad faces - mesh.Faces.AddFace(*face) - face_callback(face) - - mesh.Normals.ComputeNormals() - mesh.Compact() - return mesh diff --git a/src/compas_rhino/conversions/_primitives.py b/src/compas_rhino/conversions/_primitives.py deleted file mode 100644 index 47792e4a6a2..00000000000 --- a/src/compas_rhino/conversions/_primitives.py +++ /dev/null @@ -1,352 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.geometry import Point -from compas.geometry import Vector -from compas.geometry import Line -from compas.geometry import Plane -from compas.geometry import Frame -from compas.geometry import Circle -from compas.geometry import Ellipse -from compas.geometry import Polyline -from compas.geometry import Polygon -from compas.geometry import Arc - -from Rhino.Geometry import Point3d -from Rhino.Geometry import Vector3d -from Rhino.Geometry import Interval -from Rhino.Geometry import Line as RhinoLine -from Rhino.Geometry import Plane as RhinoPlane -from Rhino.Geometry import Circle as RhinoCircle -from Rhino.Geometry import Ellipse as RhinoEllipse -from Rhino.Geometry import Polyline as RhinoPolyline -from Rhino.Geometry import Arc as RhinoArc - - -def point_to_compas(point): - """Convert a Rhino point to a COMPAS point. - - Parameters - ---------- - point : :rhino:`Rhino.Geometry.Point3d` - - Returns - ------- - :class:`~compas.geometry.Point` - - """ - return Point(point.X, point.Y, point.Z) - - -def point_to_rhino(point): - """Convert a COMPAS point to a Rhino point. - - Parameters - ---------- - point : :class:`~compas.geometry.Point` - - Returns - ------- - :rhino:`Rhino.Geometry.Point3d` - - """ - return Point3d(point[0], point[1], point[2]) - - -def vector_to_compas(vector): - """Convert a Rhino vector to a COMPAS vector. - - Parameters - ---------- - vector : :rhino:`Rhino.Geometry.Vector3d` - - Returns - ------- - :class:`~compas.geometry.Vector` - - """ - return Vector(vector.X, vector.Y, vector.Z) - - -def vector_to_rhino(vector): - """Convert a COMPAS vector to a Rhino vector. - - Parameters - ---------- - vector : :class:`~compas.geometry.Vector` - - Returns - ------- - :rhino:`Rhino.Geometry.Vector3d` - - """ - return Vector3d(vector[0], vector[1], vector[2]) - - -def line_to_compas(line): - """Convert a Rhino line to a COMPAS line. - - Parameters - ---------- - line : :rhino:`Rhino.Geometry.Line` - - Returns - ------- - :class:`~compas.geometry.Line` - - """ - return Line(point_to_compas(line.From), point_to_compas(line.To)) - - -def line_to_rhino(line): - """Convert a COMPAS line to a Rhino line. - - Parameters - ---------- - line : :class:`~compas.geometry.Line` - - Returns - ------- - :rhino:`Rhino.Geometry.Line` - - """ - return RhinoLine(point_to_rhino(line[0]), point_to_rhino(line[1])) - - -def plane_to_compas(plane): - """Convert a Rhino plane to a COMPAS plane. - - Parameters - ---------- - plane : :rhino:`Rhino.Geometry.Plane` - - Returns - ------- - :class:`~compas.geometry.Plane` - - """ - return Plane(point_to_compas(plane.Origin), vector_to_compas(plane.Normal)) - - -def plane_to_rhino(plane): - """Convert a COMPAS plane to a Rhino plane. - - Parameters - ---------- - plane : :class:`~compas.geometry.Plane` - - Returns - ------- - :rhino:`Rhino.Geometry.Plane` - - """ - return RhinoPlane(point_to_rhino(plane[0]), vector_to_rhino(plane[1])) - - -def plane_to_compas_frame(plane): - """Convert a Rhino plane to a COMPAS frame. - - Parameters - ---------- - plane : :rhino:`Rhino.Geometry.Plane` - - Returns - ------- - :class:`~compas.geometry.Frame` - - """ - return Frame( - point_to_compas(plane.Origin), - vector_to_compas(plane.XAxis), - vector_to_compas(plane.YAxis), - ) - - -def frame_to_rhino_plane(frame): - """Convert a COMPAS frame to a Rhino plane. - - Parameters - ---------- - frame : :class:`~compas.geometry.Frame` - - Returns - ------- - :rhino:`Rhino.Geometry.Plane` - - """ - return RhinoPlane(point_to_rhino(frame.point), vector_to_rhino(frame.xaxis), vector_to_rhino(frame.yaxis)) - - -def frame_to_rhino(frame): - """Convert a COMPAS frame to a Rhino plane. - - Parameters - ---------- - frame : :class:`~compas.geometry.Frame` - - Returns - ------- - :rhino:`Rhino.Geometry.Plane` - - """ - return RhinoPlane(point_to_rhino(frame[0]), vector_to_rhino(frame[1]), vector_to_rhino(frame[2])) - - -def circle_to_compas(circle): - """Convert a Rhino circle to a COMPAS circle. - - Parameters - ---------- - circle : :rhino:`Rhino.Geometry.Circle` - - Returns - ------- - :class:`~compas.geometry.Circle` - - """ - return Circle(plane_to_compas(circle.Plane), circle.Radius) - - -def circle_to_rhino(circle): - """Convert a COMPAS circle to a Rhino circle. - - Parameters - ---------- - circle : :class:`~compas.geometry.Circle` - - Returns - ------- - :rhino:`Rhino.Geometry.Circle` - - """ - return RhinoCircle(plane_to_rhino(circle[0]), circle[1]) - - -def ellipse_to_compas(ellipse): - """Convert a Rhino ellipse to a COMPAS ellipse. - - Parameters - ---------- - ellipse : :rhino:`Rhino.Geometry.Ellipse` - - Returns - ------- - :class:`~compas.geometry.Ellipse` - - """ - return Ellipse(plane_to_compas(ellipse.Plane), ellipse.Radius1, ellipse.Radius2) - - -def ellipse_to_rhino(ellipse): - """Convert a COMPAS ellipse to a Rhino ellipse. - - Parameters - ---------- - ellipse : :class:`~compas.geometry.Ellipse` - - Returns - ------- - :rhino:`Rhino.Geometry.Ellipse` - - """ - return RhinoEllipse(plane_to_rhino(ellipse[0]), ellipse[1], ellipse[2]) - - -def polyline_to_compas(polyline): - """Convert a Rhino polyline to a COMPAS polyline. - - Parameters - ---------- - polyline : :rhino:`Rhino.Geometry.Polyline` - - Returns - ------- - :class:`~compas.geometry.Polyline` - - """ - return Polyline([point_to_compas(point) for point in polyline]) - - -def polyline_to_rhino(polyline): - """Convert a COMPAS polyline to a Rhino polyline. - - Parameters - ---------- - polyline : :class:`~compas.geometry.Polyline` - - Returns - ------- - :rhino:`Rhino.Geometry.Ellipse` - - """ - return RhinoPolyline([point_to_rhino(point) for point in polyline]) - - -def polygon_to_compas(polygon): - """Convert a Rhino polygon to a COMPAS polygon. - - Parameters - ---------- - polygon : :rhino:`Rhino.Geometry.Polygon` - - Returns - ------- - :class:`~compas.geometry.Polygon` - - """ - return Polygon([point_to_compas(point) for point in polygon]) - - -def polygon_to_rhino(polygon): - """Convert a COMPAS polygon to a Rhino polygon. - - Parameters - ---------- - polygon : :class:`~compas.geometry.Polygon` - - Returns - ------- - :rhino:`Rhino.Geometry.Polygon` - - """ - raise NotImplementedError - - -def arc_to_rhino(arc): - """Convert a COMPAS Arc to a Rhino one. - - Parameters - ---------- - arc : :class:`~compas.geometry.Arc` - The COMPAS Arc to convert. - - Returns - ------- - :rhino:`Rhino.Geometry.Arc` - - """ - plane = frame_to_rhino_plane(arc.frame) - circle = RhinoCircle(plane, arc.radius) - angle_interval = Interval(arc.start_angle, arc.end_angle) - return RhinoArc(circle, angle_interval) - - -def arc_to_compas(arc): - """Convert a Rhino Arc Structure to a COMPAS Arc. - - Parameters - ---------- - arc : :rhino:`Rhino.Geometry.Arc` - The Rhino Arc to convert. - - Returns - ------- - :class:`~compas.geometry.Arc` - - """ - frame = plane_to_compas_frame(arc.Plane) - # Arc center point can be set independently of its plane's origin - center = point_to_compas(arc.Center) - frame.point = center - return Arc(frame, arc.Radius, start_angle=arc.StartAngle, end_angle=arc.EndAngle) diff --git a/src/compas_rhino/conversions/_shapes.py b/src/compas_rhino/conversions/_shapes.py deleted file mode 100644 index 31836c5525a..00000000000 --- a/src/compas_rhino/conversions/_shapes.py +++ /dev/null @@ -1,169 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.geometry import Plane -from compas.geometry import Circle -from compas.geometry import Box -from compas.geometry import Sphere -from compas.geometry import Cone -from compas.geometry import Cylinder - -from Rhino.Geometry import Box as RhinoBox -from Rhino.Geometry import Sphere as RhinoSphere -from Rhino.Geometry import Cone as RhinoCone -from Rhino.Geometry import Cylinder as RhinoCylinder -from Rhino.Geometry import Interval - -from ._primitives import plane_to_rhino -from ._primitives import circle_to_rhino -from ._primitives import frame_to_rhino -from ._primitives import point_to_rhino -from ._primitives import plane_to_compas_frame -from ._primitives import plane_to_compas -from ._primitives import point_to_compas -from ._primitives import vector_to_compas - - -def box_to_compas(box): - """Convert a Rhino box to a COMPAS box. - - Parameters - ---------- - box: :rhino:`Rhino.Geometry.Box` - - Returns - ------- - :class:`~compas.geometry.Box` - - """ - xsize = box.X.Length - ysize = box.Y.Length - zsize = box.Z.Length - frame = plane_to_compas_frame(box.Plane) - frame.point += frame.xaxis * 0.5 * xsize - frame.point += frame.yaxis * 0.5 * ysize - frame.point += frame.zaxis * 0.5 * zsize - return Box(frame, xsize, ysize, zsize) - - -def box_to_rhino(box): - """Convert a COMPAS box to a Rhino box. - - Parameters - ---------- - box: :class:`~compas.geometry.Box` - - Returns - ------- - :rhino:`Rhino.Geometry.Box` - - """ - # compas frame is center of box, intervals are in frame space - base_plane = box.frame.copy() - base_plane.point -= base_plane.xaxis * 0.5 * box.xsize - base_plane.point -= base_plane.yaxis * 0.5 * box.ysize - base_plane.point -= base_plane.zaxis * 0.5 * box.zsize - return RhinoBox( - frame_to_rhino(base_plane), - Interval(0, box.xsize), - Interval(0, box.ysize), - Interval(0, box.zsize), - ) - - -def sphere_to_compas(sphere): - """Convert a Rhino sphere to a COMPAS sphere. - - Parameters - ---------- - sphere: :rhino:`Rhino.Geometry.Sphere` - - Returns - ------- - :class:`~compas.geometry.Sphere` - - """ - return Sphere(point_to_compas(sphere.Center), sphere.Radius) - - -def sphere_to_rhino(sphere): - """Convert a COMPAS sphere to a Rhino sphere. - - Parameters - ---------- - sphere: :class:`~compas.geometry.Sphere` - - Returns - ------- - :rhino:`Rhino.Geometry.Sphere` - - """ - return RhinoSphere(point_to_rhino(sphere.point), sphere.radius) - - -def cone_to_compas(cone): - """Convert a Rhino cone to a COMPAS cone. - - Parameters - ---------- - cone: :rhino:`Rhino.Geometry.Cone` - - Returns - ------- - :class:`~compas.geometry.Cone` - - """ - plane = Plane(cone.BasePoint, vector_to_compas(cone.Plane.Normal).inverted()) - return Cone(Circle(plane, cone.Radius), cone.Height) - - -def cone_to_rhino(cone): - """Convert a COMPAS cone to a Rhino cone. - - Parameters - ---------- - cone: :class:`~compas.geometry.Cone` - - Returns - ------- - :rhino:`Rhino.Geometry.Cone` - - """ - return RhinoCone(plane_to_rhino(cone.circle.plane), cone.height, cone.circle.radius) - - -def cylinder_to_compas(cylinder): - """Convert a Rhino cylinder to a COMPAS cylinder. - - Parameters - ---------- - cylinder: :rhino:`Rhino.Geometry.Cylinder` - - Returns - ------- - :class:`~compas.geometry.Cylinder` - - """ - plane = plane_to_compas(cylinder.BasePlane) - height = cylinder.TotalHeight - plane.point += plane.normal * (0.5 * height) - return Cylinder(Circle(plane, cylinder.Radius), height) - - -def cylinder_to_rhino(cylinder): - """Convert a COMPAS cylinder to a Rhino cylinder. - - Parameters - ---------- - cylinder: :class:`~compas.geometry.Cylinder` - - Returns - ------- - :rhino:`Rhino.Geometry.Cylinder` - - """ - circle = cylinder.circle.copy() - height = cylinder.height - circle.plane.point += circle.plane.normal * (-0.5 * height) - return RhinoCylinder(circle_to_rhino(circle), cylinder.height) diff --git a/src/compas_rhino/conversions/_surfaces.py b/src/compas_rhino/conversions/_surfaces.py deleted file mode 100644 index 7b5357c0337..00000000000 --- a/src/compas_rhino/conversions/_surfaces.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from compas.geometry import Point - -from Rhino.Geometry import NurbsSurface as RhinoNurbsSurface - -from ._primitives import point_to_rhino -from ._primitives import point_to_compas - - -def surface_to_compas_data(surface): - """Convert a Rhino surface to a COMPAS surface. - - Parameters - ---------- - surface: :rhino:`Rhino.Geometry.Surface` - - Returns - ------- - dict - - """ - surface = surface.ToNurbsSurface() - - points = [] - weights = [] - for j in range(surface.Points.VCount): - _points = [] - _weights = [] - for i in range(surface.Points.UCount): - point = surface.Points.GetPoint(i, j) - weight = surface.Points.GetWeight(i, j) - _points.append(point_to_compas(point)) - _weights.append(weight) - points.append(_points) - weights.append(_weights) - - u_knots = [] - u_mults = [] - for index in range(surface.KnotsU.Count): - u_knots.append(surface.KnotsU.Item[index]) - u_mults.append(surface.KnotsU.KnotMultiplicity(index)) - - v_knots = [] - v_mults = [] - for index in range(surface.KnotsV.Count): - v_knots.append(surface.KnotsV.Item[index]) - v_mults.append(surface.KnotsV.KnotMultiplicity(index)) - - u_degree = surface.OrderU - 1 - v_degree = surface.OrderV - 1 - - is_u_periodic = False - is_v_periodic = False - - return { - "points": [[point.data for point in row] for row in points], - "weights": weights, - "u_knots": u_knots, - "v_knots": v_knots, - "u_mults": u_mults, - "v_mults": v_mults, - "u_degree": u_degree, - "v_degree": v_degree, - "is_u_periodic": is_u_periodic, - "is_v_periodic": is_v_periodic, - } - - -def data_to_rhino_surface(data): - """Convert a COMPAS surface to a Rhino surface. - - Parameters - ---------- - data: dict - - Returns - ------- - :rhino:`Rhino.Geometry.NurbsSurface` - - """ - points = [[Point.from_data(point) for point in row] for row in data["points"]] - - nu = len(points[0]) - nv = len(points) - - nurbs = RhinoNurbsSurface.Create(3, False, data["u_degree"] + 1, data["v_degree"] + 1, nu, nv) - for i in range(nu): - for j in range(nv): - nurbs.Points.SetPoint(i, j, point_to_rhino(points[j][i])) - nurbs.Points.SetWeight(i, j, data["weights"][j][i]) - - u_knotvector = [] - for knot, mult in zip(data["u_knots"], data["u_mults"]): - for i in range(mult): - u_knotvector.append(knot) - - for index, knot in enumerate(u_knotvector): - nurbs.KnotsU.Item[index] = knot - - v_knotvector = [] - for knot, mult in zip(data["v_knots"], data["v_mults"]): - for i in range(mult): - v_knotvector.append(knot) - - for index, knot in enumerate(v_knotvector): - nurbs.KnotsV.Item[index] = knot - - return nurbs diff --git a/src/compas_rhino/conversions/_transformations.py b/src/compas_rhino/conversions/_transformations.py deleted file mode 100644 index 938a31af31e..00000000000 --- a/src/compas_rhino/conversions/_transformations.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from Rhino.Geometry import Transform - - -def xform_from_transformation(transformation): - """Creates a Rhino transformation from a COMPAS transformation. - - Parameters - ---------- - transformation : :class:`~compas.geometry.Transformation` - COMPAS transformation. - - Returns - ------- - :rhino:`Rhino.Geometry.Transform` - - """ - transform = Transform(1.0) - for i in range(0, 4): - for j in range(0, 4): - transform[i, j] = transformation[i, j] - return transform - - -xform_to_rhino = xform_from_transformation - - -def xform_from_transformation_matrix(matrix): - """Creates a Rhino transformation from a 4x4 transformation matrix. - - Parameters - ---------- - matrix : list[list[float]] - The 4x4 transformation matrix in row-major order. - - Returns - ------- - :rhino:`Rhino.Geometry.Transform` - - """ - transform = Transform(1.0) - for i in range(0, 4): - for j in range(0, 4): - transform[i, j] = matrix[i][j] - return transform - - -xform_matrix_to_rhino = xform_from_transformation_matrix - - -def xtransform(geometry, transformation): - """Transforms the Rhino Geometry object with a COMPAS transformation. - - Parameters - ---------- - geometry : :rhino:`Rhino.Geometry.GeometryBase` - Rhino Geometry object. - transformation : :class:`~compas.geometry.Transformation` - COMPAS transformation. - - Returns - ------- - None - - """ - T = xform_from_transformation(transformation) - geometry.Transform(T) - - -def xtransformed(geometry, transformation): - """Returns a copy of the transformed Rhino Geometry object. - - Parameters - ---------- - geometry : :rhino:`Rhino.Geometry.GeometryBase` - Rhino Geometry object. - transformation : :class:`~compas.geometry.Transformation` - COMPAS transformation. - - Returns - ------- - :rhino:`Rhino.Geometry.GeometryBase` - - """ - T = xform_from_transformation(transformation) - geometry = geometry.Duplicate() - geometry.Transform(T) - return geometry diff --git a/src/compas_rhino/conversions/box.py b/src/compas_rhino/conversions/box.py deleted file mode 100644 index e197aecf73c..00000000000 --- a/src/compas_rhino/conversions/box.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import Rhino - -from compas.geometry import Box - -from ._exceptions import ConversionError -from ._geometry import RhinoGeometry -from ._shapes import box_to_compas -from ._shapes import box_to_rhino - - -class RhinoBox(RhinoGeometry): - """Wrapper for Rhino boxes.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Box` | :class:`~compas.geometry.Box` - The geometry object defining a box. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a box. - """ - if not isinstance(geometry, Rhino.Geometry.Box): - if isinstance(geometry, Rhino.Geometry.Extrusion): - plane = geometry.GetPathPlane(0) - box = geometry.GetBoundingBox(plane) - geometry = Rhino.Geometry.Box(plane, box) - elif isinstance(geometry, Box): - geometry = box_to_rhino(geometry) - else: - raise ConversionError("Geometry object cannot be converted to a box: {}".format(geometry)) - - self._geometry = geometry - - def to_compas(self): - """Convert to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Box` - A COMPAS box. - """ - return box_to_compas(self.geometry) diff --git a/src/compas_rhino/conversions/breps.py b/src/compas_rhino/conversions/breps.py new file mode 100644 index 00000000000..e2fa73de207 --- /dev/null +++ b/src/compas_rhino/conversions/breps.py @@ -0,0 +1,124 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import scriptcontext as sc # type: ignore + +from .exceptions import ConversionError + +from .shapes import cone_to_compas +from .shapes import cylinder_to_compas +from .shapes import sphere_to_compas + + +# ============================================================================= +# To Rhino +# ============================================================================= + + +def brep_to_rhino(brep): + """Convert a COMPAS brep to a Rhino brep. + + Parameters + ---------- + brep : :class:`~compas.geometry.Brep` + + Returns + ------- + :rhino:`Rhino.Geometry.Brep` + + """ + return brep.native_brep + + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def brep_to_compas_box(brep): + """Convert a Rhino brep to a COMPAS box. + + Parameters + ---------- + brep : :rhino:`Rhino.Geometry.Brep` + + Returns + ------- + :class:`~compas.geometry.Box` + + """ + raise NotImplementedError + + +def brep_to_compas_cone(brep): + """Convert a Rhino brep to a COMPAS cone. + + Parameters + ---------- + brep : :rhino:`Rhino.Geometry.Brep` + + Returns + ------- + :class:`~compas.geometry.Cone` + + """ + if brep.Faces.Count > 2: + raise ConversionError("Object brep cannot be converted to a cone.") + + for face in brep.Faces: + if face.IsCone(): + result, cone = face.TryGetCone() + if result: + return cone_to_compas(cone) + + +def brep_to_compas_cylinder(brep, tol=None): + """Convert a Rhino brep to a COMPAS cylinder. + + Parameters + ---------- + brep : :rhino:`Rhino.Geometry.Brep` + + Returns + ------- + :class:`~compas.geometry.Cylinder` + + """ + tol = tol or sc.doc.ModelAbsoluteTolerance + + if brep.Faces.Count > 3: + raise ConversionError("Object brep cannot be converted to a cylinder.") + + for face in brep.Faces: + # being too strict about what is considered a cylinder + # results in cylinders created by Rhino itself + # to not be recognized... + if face.IsCylinder(tol): + result, cylinder = face.TryGetFiniteCylinder(tol) + if result: + return cylinder_to_compas(cylinder) + + +def brep_to_compas_sphere(brep): + """Convert a Rhino brep to a COMPAS sphere. + + Parameters + ---------- + brep : :rhino:`Rhino.Geometry.Brep` + + Returns + ------- + :class:`~compas.geometry.Sphere` + + """ + if brep.Faces.Count != 1: + raise ConversionError("Brep cannot be converted to a sphere.") + + face = brep.Faces.Item[0] + if not face.IsSphere(): + raise ConversionError("Brep cannot be converted to a sphere.") + + result, sphere = face.TryGetSphere() + if result: + return sphere_to_compas(sphere) diff --git a/src/compas_rhino/conversions/circle.py b/src/compas_rhino/conversions/circle.py deleted file mode 100644 index db09d91df6d..00000000000 --- a/src/compas_rhino/conversions/circle.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from ._primitives import circle_to_compas -from ._primitives import circle_to_rhino - -from ._geometry import RhinoGeometry - - -class RhinoCircle(RhinoGeometry): - """Wrapper for Rhino circles.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Circle` | :class:`~compas.geometry.Circle` - The geometry object defining a circle. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a box. - """ - if not isinstance(geometry, Rhino.Geometry.Circle): - geometry = circle_to_rhino(geometry) - self._geometry = geometry - - def to_compas(self): - """Convert to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Circle` - A COMPAS circle. - """ - return circle_to_compas(self.geometry) diff --git a/src/compas_rhino/conversions/cone.py b/src/compas_rhino/conversions/cone.py deleted file mode 100644 index 2488cb4127e..00000000000 --- a/src/compas_rhino/conversions/cone.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from compas.geometry import Cone - -from ._exceptions import ConversionError - -from ._shapes import cone_to_rhino -from ._shapes import cone_to_compas - -from ._geometry import RhinoGeometry - - -class RhinoCone(RhinoGeometry): - """Wrapper for Rhino cones.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Cone` | :class:`~compas.geometry.Cone` - The geometry object defining a cone. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a cone. - """ - if not isinstance(geometry, Rhino.Geometry.Cone): - if isinstance(geometry, Rhino.Geometry.Brep): - if geometry.Faces.Count > 2: - raise ConversionError("Object brep cannot be converted to a cone.") - faces = geometry.Faces - geometry = None - for face in faces: - if face.IsCone(): - result, geometry = face.TryGetCone() - if result: - break - if not geometry: - raise ConversionError("Object brep cannot be converted to a cone.") - elif isinstance(geometry, Cone): - geometry = cone_to_rhino(geometry) - else: - raise ConversionError("Geometry object cannot be converted to a cone: {}".format(geometry)) - self._geometry = geometry - - def to_compas(self): - """Convert to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Cone` - A COMPAS cone. - """ - return cone_to_compas(self.geometry) diff --git a/src/compas_rhino/conversions/curve.py b/src/compas_rhino/conversions/curve.py deleted file mode 100644 index 88c626a3eed..00000000000 --- a/src/compas_rhino/conversions/curve.py +++ /dev/null @@ -1,147 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from compas.geometry import Line -from compas.geometry import Circle -from compas.geometry import Ellipse - -from ._exceptions import ConversionError - -from ._curves import circle_to_rhino_curve -from ._curves import ellipse_to_rhino_curve -from ._curves import line_to_rhino_curve -from ._curves import curve_to_compas_circle -from ._curves import curve_to_compas_ellipse -from ._curves import curve_to_compas_polyline -from ._curves import curve_to_compas_line - -from ._geometry import RhinoGeometry - - -class RhinoCurve(RhinoGeometry): - """Wrapper for Rhino curves.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Curve` | :class:`~compas.geometry.Line` | :class:`~compas.geometry.Circle` | :class:`~compas.geometry.Ellipse` - The geometry object defining a curve. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a curve. - """ - if not isinstance(geometry, Rhino.Geometry.Curve): - if isinstance(geometry, Line): - geometry = line_to_rhino_curve(geometry) - elif isinstance(geometry, Circle): - geometry = circle_to_rhino_curve(geometry) - elif isinstance(geometry, Ellipse): - geometry = ellipse_to_rhino_curve(geometry) - else: - raise ConversionError("The geometry cannot be converted to a curve.") - self._geometry = geometry - - def to_compas(self): - """Convert the curve to a COMPAS curve. - - Returns - ------- - :class:`~compas_rhino.geometry.RhinoNurbsCurve` - """ - from compas_rhino.geometry import RhinoNurbsCurve - - curve = RhinoNurbsCurve.from_rhino(self.geometry) - return curve - - def to_compas_circle(self): - """Convert the curve to a COMPAS circle. - - Returns - ------- - :class:`~compas.geometry.Circle` - """ - return curve_to_compas_circle(self.geometry) - - def to_compas_ellipse(self): - """Convert the curve to a COMPAS ellipse. - - Returns - ------- - :class:`~compas.geometry.Ellipse` - """ - return curve_to_compas_ellipse(self.geometry) - - def to_compas_line(self): - """Convert the curve to a COMPAS line. - - Returns - ------- - :class:`~compas.geometry.Line` - """ - return curve_to_compas_line(self.geometry) - - def to_compas_polyline(self): - """Convert the curve to a COMPAS polyline. - - Returns - ------- - :class:`~compas.geometry.Polyline` - """ - return curve_to_compas_polyline(self.geometry) - - def closest_point(self, point, maxdist=0.0, return_param=False): - """Compute the closest point on a curve to a point in space. - - Parameters - ---------- - point : point - A point location. - maxdist : float, optional - The maximum distance between the point on the curve and the curve. - Default is ``0.0``. - return_param : bool, optional - Return not only the point coordinates, but also the parameter of the point on the curve. - Default is False. - - Returns - ------- - list - The XYZ coordinates of the closest point, if ``return_param`` is False. - The XYZ coordinates of the closest point and the curve parameter, if ``return_param`` is True. - - """ - rc, t = self.geometry.ClosestPoint(Rhino.Geometry.Point3d(*point), maxdist) - x, y, z = list(self.geometry.PointAt(t)) - if not return_param: - return [x, y, z] - return [x, y, z, t] - - def closest_points(self, points, maxdist=0.0): - """Compute the closest points on the curve to a list of point locations. - - Parameters - ---------- - points : list - The point locations. - maxdist : float, optional - The maximum distance between the closest points and the curve. - Default is ``0.0``. - - Returns - ------- - list - A list of closest point locations. - """ - return [self.closest_point(point, maxdist) for point in points] diff --git a/src/compas_rhino/conversions/curves.py b/src/compas_rhino/conversions/curves.py new file mode 100644 index 00000000000..393d9b0608e --- /dev/null +++ b/src/compas_rhino/conversions/curves.py @@ -0,0 +1,441 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import scriptcontext as sc # type: ignore + +from Rhino.Geometry import Interval # type: ignore +from Rhino.Geometry import NurbsCurve as RhinoNurbsCurve # type: ignore +from Rhino.Geometry import Line as RhinoLine # type: ignore +from Rhino.Geometry import Circle as RhinoCircle # type: ignore +from Rhino.Geometry import Ellipse as RhinoEllipse # type: ignore +from Rhino.Geometry import Polyline as RhinoPolyline # type: ignore +from Rhino.Geometry import PolylineCurve as RhinoPolylineCurve # type: ignore +from Rhino.Geometry import Arc as RhinoArc # type: ignore +from Rhino.DocObjects import RhinoObject # type: ignore + +from compas.geometry import Line +from compas.geometry import Circle +from compas.geometry import Ellipse +from compas.geometry import Polyline +from compas.geometry import Arc + +from .exceptions import ConversionError + +from .geometry import point_to_rhino +from .geometry import plane_to_rhino +from .geometry import frame_to_rhino_plane + +from .geometry import point_to_compas +from .geometry import plane_to_compas +from .geometry import plane_to_compas_frame + + +# ============================================================================= +# To Rhino +# ============================================================================= + + +def data_to_rhino_curve(data): + """Convert a COMPAS curve to a Rhino curve. + + Parameters + ---------- + data : dict + + Returns + ------- + :rhino:`Rhino.Geometry.NurbsCurve` + + """ + nurbs = RhinoNurbsCurve(data["degree"], len(data["points"])) + + for index, xyz in enumerate(data["points"]): + nurbs.Points.SetPoint(index, *xyz) + + knotvector = [] + for knot, mult in zip(data["knots"], data["multiplicities"]): + for i in range(mult): + knotvector.append(knot) + + for index, knot in enumerate(knotvector): + nurbs.Knots.Item[index] = knot + return nurbs + + +def line_to_rhino(line): + """Convert a COMPAS line to a Rhino line. + + Parameters + ---------- + line : :class:`~compas.geometry.Line` + + Returns + ------- + :rhino:`Rhino.Geometry.Line` + + """ + return RhinoLine(point_to_rhino(line[0]), point_to_rhino(line[1])) + + +def line_to_rhino_curve(line): + """Convert a COMPAS line to a Rhino curve. + + Parameters + ---------- + line : :class:`~compas.geometry.Line` + + Returns + ------- + :rhino:`Rhino.Geometry.Curve` + + """ + return RhinoNurbsCurve.CreateFromLine(line_to_rhino(line)) + + +def polyline_to_rhino(polyline, tol=None): + """Convert a COMPAS polyline to a Rhino polyline. + + Parameters + ---------- + polyline : :class:`~compas.geometry.Polyline` + + Returns + ------- + :rhino:`Rhino.Geometry.Ellipse` + + """ + tol = tol or sc.doc.ModelAbsoluteTolerance + polyline = RhinoPolyline([point_to_rhino(point) for point in polyline]) + polyline.DeleteShortSegments(tol) + return polyline + + +def polyline_to_rhino_curve(polyline): + """Convert a COMPAS polyline to a Rhino curve. + + Parameters + ---------- + polyline : :class:`~compas.geometry.Polyline` + + Returns + ------- + :rhino:`Rhino.Geometry.PolylineCurve` + + """ + return RhinoPolylineCurve([point_to_rhino(point) for point in polyline]) + + +def circle_to_rhino(circle): + """Convert a COMPAS circle to a Rhino circle. + + Parameters + ---------- + circle : :class:`~compas.geometry.Circle` + + Returns + ------- + :rhino:`Rhino.Geometry.Circle` + + """ + return RhinoCircle(plane_to_rhino(circle.plane), circle.radius) + + +def circle_to_rhino_curve(circle): + """Convert a COMPAS circle to a Rhino curve. + + Parameters + ---------- + circle : :class:`~compas.geometry.Circle` + + Returns + ------- + :rhino:`Rhino.Geometry.Curve` + + """ + return RhinoNurbsCurve.CreateFromCircle(circle_to_rhino(circle)) + + +def ellipse_to_rhino(ellipse): + """Convert a COMPAS ellipse to a Rhino ellipse. + + Parameters + ---------- + ellipse : :class:`~compas.geometry.Ellipse` + + Returns + ------- + :rhino:`Rhino.Geometry.Ellipse` + + """ + return RhinoEllipse(plane_to_rhino(ellipse.plane), ellipse.major, ellipse.minor) + + +def ellipse_to_rhino_curve(ellipse): + """Convert a COMPAS ellipse to a Rhino curve. + + Parameters + ---------- + ellipse : :class:`~compas.geometry.Ellipse` + + Returns + ------- + :rhino:`Rhino.Geometry.Curve` + + """ + return RhinoNurbsCurve.CreateFromEllipse(ellipse_to_rhino(ellipse)) + + +def arc_to_rhino(arc): + """Convert a COMPAS Arc to a Rhino one. + + Parameters + ---------- + arc : :class:`~compas.geometry.Arc` + The COMPAS Arc to convert. + + Returns + ------- + :rhino:`Rhino.Geometry.Arc` + + """ + plane = frame_to_rhino_plane(arc.frame) + circle = RhinoCircle(plane, arc.radius) + angle_interval = Interval(arc.start_angle, arc.end_angle) + return RhinoArc(circle, angle_interval) + + +def curve_to_rhino(curve): + """Convert a COMPAS curve to a Rhino curve. + + Parameters + ---------- + curve : :class:`~compas.geometry.Curve` + + Returns + ------- + :rhino:`Rhino.Geometry.Curve` + + """ + return curve.rhino_curve + + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def line_to_compas(line): + """Convert a Rhino line to a COMPAS line. + + Parameters + ---------- + line : :rhino:`Rhino.Geometry.Line` + + Returns + ------- + :class:`~compas.geometry.Line` + + """ + return Line(point_to_compas(line.From), point_to_compas(line.To)) + + +def circle_to_compas(circle): + """Convert a Rhino circle to a COMPAS circle. + + Parameters + ---------- + circle : :rhino:`Rhino.Geometry.Circle` + + Returns + ------- + :class:`~compas.geometry.Circle` + + """ + frame = plane_to_compas(circle.Plane) + return Circle(circle.Radius, frame=frame) + + +def ellipse_to_compas(ellipse): + """Convert a Rhino ellipse to a COMPAS ellipse. + + Parameters + ---------- + ellipse : :rhino:`Rhino.Geometry.Ellipse` + + Returns + ------- + :class:`~compas.geometry.Ellipse` + + """ + frame = plane_to_compas(ellipse.Plane) + return Ellipse(ellipse.Radius1, ellipse.Radius2, frame=frame) + + +def polyline_to_compas(polyline): + """Convert a Rhino polyline to a COMPAS polyline. + + Parameters + ---------- + polyline : :rhino:`Rhino.Geometry.Polyline` + + Returns + ------- + :class:`~compas.geometry.Polyline` + + """ + return Polyline([point_to_compas(point) for point in polyline]) + + +def arc_to_compas(arc): + """Convert a Rhino Arc Structure to a COMPAS Arc. + + Parameters + ---------- + arc : :rhino:`Rhino.Geometry.Arc` + The Rhino Arc to convert. + + Returns + ------- + :class:`~compas.geometry.Arc` + + """ + frame = plane_to_compas_frame(arc.Plane) + # Arc center point can be set independently of its plane's origin + center = point_to_compas(arc.Center) + frame.point = center + return Arc(radius=arc.Radius, start_angle=arc.StartAngle, end_angle=arc.EndAngle, frame=frame) + + +def curve_to_compas_line(curve): + """Convert a Rhino curve to a COMPAS line. + + Parameters + ---------- + curve : :rhino:`Rhino.Geometry.Curve` + + Returns + ------- + :class:`~compas.geometry.Line` + + """ + if isinstance(curve, RhinoObject): + curve = curve.Geometry + return Line(point_to_compas(curve.PointAtStart), point_to_compas(curve.PointAtEnd)) + + +def curve_to_compas_circle(curve): + """Convert a Rhino curve to a COMPAS circle. + + Parameters + ---------- + curve : :rhino:`Rhino.Geometry.Curve` + + Returns + ------- + :class:`~compas.geometry.Circle` + + Raises + ------ + ConversionError + If the curve cannot be converted to a circle. + + """ + if isinstance(curve, RhinoObject): + curve = curve.Geometry + result, circle = curve.TryGetCircle() + if not result: + raise ConversionError("The curve cannot be converted to a circle.") + return circle_to_compas(circle) + + +def curve_to_compas_ellipse(curve): + """Convert a Rhino curve to a COMPAS ellipse. + + Parameters + ---------- + curve : :rhino:`Rhino.Geometry.Curve` + + Returns + ------- + :class:`~compas.geometry.Ellipse` + + Raises + ------ + ConversionError + If the curve cannot be converted to an ellipse. + + """ + if isinstance(curve, RhinoObject): + curve = curve.Geometry + result, ellipse = curve.TryGetEllipse() + if not result: + raise ConversionError("The curve cannot be converted to an ellipse.") + return ellipse_to_compas(ellipse) + + +def curve_to_compas_polyline(curve): + """Convert a Rhino curve to a COMPAS polyline. + + Parameters + ---------- + curve : :rhino:`Rhino.Geometry.Curve` + + Returns + ------- + :class:`~compas.geometry.Polyline` + + Raises + ------ + ConversionError + If the curve cannot be converted to a polyline. + + """ + if isinstance(curve, RhinoObject): + curve = curve.Geometry + result, polyline = curve.TryGetPolyline() + if not result: + raise ConversionError("The curve cannot be converted to a polyline.") + return polyline_to_compas(polyline) + + +def curve_to_compas_data(curve): + """Convert a Rhino curve to a COMPAS data dict. + + Parameters + ---------- + curve : :rhino:`Rhino.Geometry.Curve` + + Returns + ------- + dict + + """ + if isinstance(curve, RhinoObject): + curve = curve.Geometry + + nurbs = curve.ToNurbsCurve() + points = [] + weights = [] + knots = [] + multiplicities = [] + degree = nurbs.Degree + is_periodic = nurbs.IsPeriodic + + for index in range(nurbs.Points.Count): + point = nurbs.Points.Item[index] + points.append(point_to_compas(point.Location)) + weights.append(point.Weight) + + for index in range(nurbs.Knots.Count): + knots.append(nurbs.Knots.Item[index]) + multiplicities.append(nurbs.Knots.KnotMultiplicity(index)) + + return { + "points": [point.data for point in points], + "weights": weights, + "knots": knots, + "multiplicities": multiplicities, + "degree": degree, + "is_periodic": is_periodic, + } diff --git a/src/compas_rhino/conversions/cylinder.py b/src/compas_rhino/conversions/cylinder.py deleted file mode 100644 index 4ccd1d1e159..00000000000 --- a/src/compas_rhino/conversions/cylinder.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from compas.geometry import Cylinder - -from ._exceptions import ConversionError - -from ._shapes import cylinder_to_rhino -from ._shapes import cylinder_to_compas - -from ._geometry import RhinoGeometry - - -class RhinoCylinder(RhinoGeometry): - """Wrapper for Rhino cylinders.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Cylinder` | :class:`~compas.geometry.Cylinder` - The geometry object defining a cylinder. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a cylinder. - """ - if not isinstance(geometry, Rhino.Geometry.Cylinder): - if isinstance(geometry, Rhino.Geometry.Extrusion): - geometry = geometry.ToBrep() - if isinstance(geometry, Rhino.Geometry.Brep): - if geometry.Faces.Count > 3: - raise ConversionError("Object brep cannot be converted to a cylinder.") - faces = geometry.Faces - geometry = None - for face in faces: - # being too strict about what is considered a cylinder - # results in cylinders created by Rhino itself - # to not be recognized... - if face.IsCylinder(0.001): - result, geometry = face.TryGetFiniteCylinder(0.001) - if result: - break - if not geometry: - raise ConversionError("Object brep cannot be converted to a cylinder.") - elif isinstance(geometry, Cylinder): - geometry = cylinder_to_rhino(geometry) - else: - raise ConversionError("Geometry object cannot be converted to a cylinder: {}".format(geometry)) - self._geometry = geometry - - def to_compas(self): - """Convert to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Cylinder` - A COMPAS cylinder. - """ - return cylinder_to_compas(self.geometry) diff --git a/src/compas_rhino/conversions/ellipse.py b/src/compas_rhino/conversions/ellipse.py deleted file mode 100644 index d2a233b0971..00000000000 --- a/src/compas_rhino/conversions/ellipse.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from ._primitives import ellipse_to_compas -from ._primitives import ellipse_to_rhino - -from ._geometry import RhinoGeometry - - -class RhinoEllipse(RhinoGeometry): - """Wrapper for Rhino ellipses.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Ellipse` | :class:`~compas.geometry.Ellipse` - The geometry object defining an ellipse. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to an ellipse. - """ - if not isinstance(geometry, Rhino.Geometry.Ellipse): - geometry = ellipse_to_rhino(geometry) - self._geometry = geometry - - def to_compas(self): - """Convert to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Ellipse` - A COMPAS ellipse. - """ - return ellipse_to_compas(self.geometry) diff --git a/src/compas_rhino/conversions/_exceptions.py b/src/compas_rhino/conversions/exceptions.py similarity index 100% rename from src/compas_rhino/conversions/_exceptions.py rename to src/compas_rhino/conversions/exceptions.py diff --git a/src/compas_rhino/conversions/extrusions.py b/src/compas_rhino/conversions/extrusions.py new file mode 100644 index 00000000000..aac62383e0a --- /dev/null +++ b/src/compas_rhino/conversions/extrusions.py @@ -0,0 +1,82 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import scriptcontext as sc # type: ignore + +from Rhino.Geometry import Box as RhinoBox # type: ignore + +from .shapes import box_to_compas +from .shapes import cylinder_to_compas +from .shapes import torus_to_compas + + +# ============================================================================= +# To Rhino +# ============================================================================= + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def extrusion_to_compas_box(extrusion): + """Convert a Rhino extrusion to a COMPAS box. + + Parameters + ---------- + extrusion : :rhino:`Rhino.Geometry.Extrusion` + + Returns + ------- + :class:`~compas.geometry.Box` + + """ + plane = extrusion.GetPathPlane(0) + bbox = extrusion.GetBoundingBox(plane) + box = RhinoBox(plane, bbox) + return box_to_compas(box) + + +def extrusion_to_compas_cylinder(extrusion, tol=None): + """Convert a Rhino extrusion to a COMPAS cylinder. + + Parameters + ---------- + extrusion : :rhino:`Rhino.Geometry.Extrusion` + tol : float, optional + The tolerance used to determine if the extrusion is a cylindrical surface. + Default is the absolute tolerance of the current doc. + + Returns + ------- + :class:`~compas.geometry.Cylinder` + + """ + tol = tol or sc.doc.ModelAbsoluteTolerance + if extrusion.IsCylinder(tol): + result, cylinder = extrusion.TryGetFiniteCylinder(tol) + if result: + return cylinder_to_compas(cylinder) + + +def extrusion_to_compas_torus(extrusion, tol=None): + """Convert a Rhino extrusion to a COMPAS torus. + + Parameters + ---------- + extrusion : :rhino:`Rhino.Geometry.Extrusion` + tol : float, optional + The tolerance used to determine if the extrusion is a toroidal surface. + Default is the absolute tolerance of the current doc. + + Returns + ------- + :class:`~compas.geometry.Torus` + + """ + tol = tol or sc.doc.ModelAbsoluteTolerance + if extrusion.IsTorus(tol): + result, torus = extrusion.TryGetTorus(tol) + if result: + return torus_to_compas(torus) diff --git a/src/compas_rhino/conversions/geometry.py b/src/compas_rhino/conversions/geometry.py new file mode 100644 index 00000000000..9184c3e2e62 --- /dev/null +++ b/src/compas_rhino/conversions/geometry.py @@ -0,0 +1,192 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from Rhino.Geometry import Point3d # type: ignore +from Rhino.Geometry import Vector3d # type: ignore +from Rhino.Geometry import Plane as RhinoPlane # type: ignore + +from compas.geometry import Point +from compas.geometry import Vector +from compas.geometry import Plane +from compas.geometry import Frame +from compas.geometry import Polygon + + +# ============================================================================= +# To Rhino +# ============================================================================= + + +def point_to_rhino(point): + """Convert a COMPAS point to a Rhino point. + + Parameters + ---------- + point : :class:`~compas.geometry.Point` + + Returns + ------- + :rhino:`Rhino.Geometry.Point3d` + + """ + return Point3d(point[0], point[1], point[2]) + + +def vector_to_rhino(vector): + """Convert a COMPAS vector to a Rhino vector. + + Parameters + ---------- + vector : :class:`~compas.geometry.Vector` + + Returns + ------- + :rhino:`Rhino.Geometry.Vector3d` + + """ + return Vector3d(vector[0], vector[1], vector[2]) + + +def plane_to_rhino(plane): + """Convert a COMPAS plane to a Rhino plane. + + Parameters + ---------- + plane : :class:`~compas.geometry.Plane` + + Returns + ------- + :rhino:`Rhino.Geometry.Plane` + + """ + return RhinoPlane(point_to_rhino(plane[0]), vector_to_rhino(plane[1])) + + +def frame_to_rhino_plane(frame): + """Convert a COMPAS frame to a Rhino plane. + + Parameters + ---------- + frame : :class:`~compas.geometry.Frame` + + Returns + ------- + :rhino:`Rhino.Geometry.Plane` + + """ + return RhinoPlane(point_to_rhino(frame.point), vector_to_rhino(frame.xaxis), vector_to_rhino(frame.yaxis)) + + +def frame_to_rhino(frame): + """Convert a COMPAS frame to a Rhino plane. + + Parameters + ---------- + frame : :class:`~compas.geometry.Frame` + + Returns + ------- + :rhino:`Rhino.Geometry.Plane` + + """ + return RhinoPlane(point_to_rhino(frame[0]), vector_to_rhino(frame[1]), vector_to_rhino(frame[2])) + + +def polygon_to_rhino(polygon): + """Convert a COMPAS polygon to a Rhino polygon. + + Parameters + ---------- + polygon : :class:`~compas.geometry.Polygon` + + Returns + ------- + :rhino:`Rhino.Geometry.Polygon` + + """ + raise NotImplementedError + + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def point_to_compas(point): + """Convert a Rhino point to a COMPAS point. + + Parameters + ---------- + point : :rhino:`Rhino.Geometry.Point3d` + + Returns + ------- + :class:`~compas.geometry.Point` + + """ + return Point(point.X, point.Y, point.Z) + + +def vector_to_compas(vector): + """Convert a Rhino vector to a COMPAS vector. + + Parameters + ---------- + vector : :rhino:`Rhino.Geometry.Vector3d` + + Returns + ------- + :class:`~compas.geometry.Vector` + + """ + return Vector(vector.X, vector.Y, vector.Z) + + +def plane_to_compas(plane): + """Convert a Rhino plane to a COMPAS plane. + + Parameters + ---------- + plane : :rhino:`Rhino.Geometry.Plane` + + Returns + ------- + :class:`~compas.geometry.Plane` + + """ + return Plane(point_to_compas(plane.Origin), vector_to_compas(plane.Normal)) + + +def plane_to_compas_frame(plane): + """Convert a Rhino plane to a COMPAS frame. + + Parameters + ---------- + plane : :rhino:`Rhino.Geometry.Plane` + + Returns + ------- + :class:`~compas.geometry.Frame` + + """ + return Frame( + point_to_compas(plane.Origin), + vector_to_compas(plane.XAxis), + vector_to_compas(plane.YAxis), + ) + + +def polygon_to_compas(polygon): + """Convert a Rhino polygon to a COMPAS polygon. + + Parameters + ---------- + polygon : :rhino:`Rhino.Geometry.Polygon` + + Returns + ------- + :class:`~compas.geometry.Polygon` + + """ + return Polygon([point_to_compas(point) for point in polygon]) diff --git a/src/compas_rhino/conversions/line.py b/src/compas_rhino/conversions/line.py deleted file mode 100644 index c0f27162175..00000000000 --- a/src/compas_rhino/conversions/line.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from ._primitives import line_to_compas -from ._primitives import line_to_rhino - -from ._curves import curve_to_compas_line - -from ._geometry import RhinoGeometry - - -class RhinoLine(RhinoGeometry): - """Wrapper for Rhino lines.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Line` | :class:`~compas.geometry.Line` - The input geometry. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a line. - """ - if not isinstance(geometry, Rhino.Geometry.Line): - if isinstance(geometry, Rhino.Geometry.Curve): - geometry = curve_to_compas_line(geometry) - geometry = line_to_rhino(geometry) - self._geometry = geometry - - def to_compas(self): - """Convert the line to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Line` - A COMPAS line. - """ - return line_to_compas(self.geometry) diff --git a/src/compas_rhino/conversions/mesh.py b/src/compas_rhino/conversions/mesh.py deleted file mode 100644 index de8dba9e14d..00000000000 --- a/src/compas_rhino/conversions/mesh.py +++ /dev/null @@ -1,146 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from compas.datastructures import Mesh - -from ._geometry import RhinoGeometry - - -class RhinoMesh(RhinoGeometry): - """Wrapper for Rhino meshes.""" - - @property - def vertices(self): - if self.geometry: - vertices = [[point.X, point.Y, point.Z] for point in self.geometry.Vertices] - else: - vertices = [] - return vertices - - @property - def faces(self): - if self.geometry: - faces = [ - [face.A, face.B, face.C] if face.IsTriangle else [face.A, face.B, face.C, face.D] - for face in self.geometry.Faces - ] - else: - faces = [] - return faces - - @property - def vertexnormals(self): - if self.geometry: - # self.geometry.ComputeNormals() - # self.geometry.UnitizeNormals() - normals = [[vector.X, vector.Y, vector.Z] for vector in self.geometry.Normals] - else: - normals = [] - return normals - - @property - def facenormals(self): - if self.geometry: - # self.geometry.ComputeFaceNormals() - # self.geometry.UnitizeFaceNormals() - normals = [[vector.X, vector.Y, vector.Z] for vector in self.geometry.FaceNormals] - else: - normals = [] - return normals - - @property - def vertexcolors(self): - if self.geometry: - colors = [[color.R, color.G, color.B] for color in self.geometry.VertexColors] - else: - colors = [] - return colors - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry: :rhino:`Rhino_Geometry_Mesh` - A Rhino mesh geometry. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a mesh. - """ - self._geometry = geometry - - def to_compas(self, cls=None): - """Convert a Rhino mesh to a COMPAS mesh. - - Parameters - ---------- - cls: :class:`~compas.datastructures.Mesh`, optional - The mesh type. - - Returns - ------- - :class:`~compas.datastructures.Mesh` - The equivalent COMPAS mesh. - """ - cls = cls or Mesh - mesh = cls() - - for vertex in self.geometry.Vertices: - mesh.add_vertex(attr_dict=dict(x=float(vertex.X), y=float(vertex.Y), z=float(vertex.Z))) - - for face in self.geometry.Faces: - if face.A == face.D or face.C == face.D: - mesh.add_face([face.A, face.B, face.C]) - else: - mesh.add_face([face.A, face.B, face.C, face.D]) - - mesh.name = self.name - - return mesh - - def closest_point(self, point, maxdist=0.0): - """Compute the closest point on the mesh to a given point. - - Parameters - ---------- - point: point - A point location. - maxdist: float, optional - The maximum distance between the closest point and the mesh. - Default is ``0.0``. - - Returns - ------- - list - The XYZ coordinates of the closest point. - """ - face, point = self.geometry.ClosestPoint(Rhino.Geometry.Point3d(*point), maxdist) - return list(point) - - def closest_points(self, points, maxdist=None): - """Compute the closest points on the mesh to a list of input points. - - Parameters - ---------- - points : list of point - The input points. - maxdist : float, optional - The maximum distance between the closest point and the mesh. - Default is ``0.0``. - - Returns - ------- - list of point - The XYZ coordinates of the closest points. - """ - return [self.closest_point(point, maxdist) for point in points] diff --git a/src/compas_rhino/conversions/meshes.py b/src/compas_rhino/conversions/meshes.py new file mode 100644 index 00000000000..9d3284cd12c --- /dev/null +++ b/src/compas_rhino/conversions/meshes.py @@ -0,0 +1,278 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from itertools import izip_longest # type: ignore + +from System.Drawing import Color as SystemColor # type: ignore +from System.Array import CreateInstance # type: ignore +from Rhino.Geometry import Mesh as RhinoMesh # type: ignore + +try: + # MeshNgon is not available in older versions of Rhino + from Rhino.Geometry import MeshNgon # type: ignore +except ImportError: + MeshNgon = None + +from compas.colors import Color +from compas.datastructures import Mesh +from compas.geometry import centroid_polygon +from compas.utilities import pairwise +from .geometry import vector_to_compas + + +def average_color(colors): + c = len(colors) + r, g, b = zip(*colors) + r = sum(r) / c + g = sum(g) / c + b = sum(b) / c + return int(r), int(g), int(b) + + +def connected_ngon(face, vertices, rmesh): + points = [vertices[index] for index in face] + centroid = centroid_polygon(points) + + c = rmesh.Vertices.Add(*centroid) + + facets = [] + for i, j in pairwise(face + face[:1]): + facets.append(rmesh.Faces.AddFace(i, j, c)) + + ngon = MeshNgon.Create(face, facets) # type: ignore + rmesh.Ngons.AddNgon(ngon) + + +def disjoint_ngon(face, vertices, rmesh): + points = [vertices[vertex] for vertex in face] + centroid = centroid_polygon(points) + + indices = [] + for point in points: + x, y, z = point + indices.append(rmesh.Vertices.Add(x, y, z)) + + c = rmesh.Vertices.Add(*centroid) + + facets = [] + for i, j in pairwise(indices + indices[:1]): + facets.append(rmesh.Faces.AddFace(i, j, c)) + + ngon = MeshNgon.Create(indices, facets) # type: ignore + rmesh.Ngons.AddNgon(ngon) + + +def disjoint_face(face, vertices, rmesh): + indices = [] + for index in face: + x, y, z = vertices[index] + indices.append(rmesh.Vertices.Add(x, y, z)) + rmesh.Faces.AddFace(*indices) + + +# ============================================================================= +# To Rhino +# ============================================================================= + + +def mesh_to_rhino( + mesh, + color=None, + vertexcolors=None, + facecolors=None, + disjoint=True, + face_callback=None, +): + """Convert a COMPAS Mesh or a Polyhedron to a Rhino mesh object. + + Parameters + ---------- + mesh : :class:`compas.datastructures.Mesh` | :class:`compas.geometry.Polyhedron` + A COMPAS Mesh or a Polyhedron. + disjoint : bool, optional + If ``True``, each face of the resulting mesh will be independently defined (have a copy of its own vertices). + face_callback : callable, optional + Called after each face is created with the face as an agrument, useful for custom post-processing. + + Returns + ------- + :class:`Rhino.Geometry.Mesh` + A Rhino mesh object. + + """ + vertices, faces = mesh.to_vertices_and_faces() + return vertices_and_faces_to_rhino( + vertices, + faces, + color=color, + vertexcolors=vertexcolors, + facecolors=facecolors, + disjoint=disjoint, + face_callback=face_callback, + ) + + +polyhedron_to_rhino = mesh_to_rhino + + +def vertices_and_faces_to_rhino( + vertices, + faces, + color=None, + vertexcolors=None, + facecolors=None, + disjoint=True, + face_callback=None, +): + """Convert COMPAS vertices and faces to a Rhino mesh object. + + Parameters + ---------- + vertices : list[[float, float, float] | :class:`~compas.geometry.Point`] + A list of point locations. + faces : list[list[int]] + A list of faces as lists of indices into `vertices`. + disjoint : bool, optional + If ``True``, each face of the resulting mesh will be independently defined (have a copy of its own vertices). + face_callback : callable, optional + Called after each face is created with the face as an agrument, useful for custom post-processing. + + Returns + ------- + :class:`Rhino.Geometry.Mesh` + A Rhino mesh object. + + """ + if disjoint and facecolors: + if len(faces) != len(facecolors): + raise ValueError("The number of face colors does not match the number of faces.") + + if not disjoint and vertexcolors: + if len(vertices) != len(vertexcolors): + raise ValueError("The number of vertex colors does not match the number of vertices.") + + face_callback = face_callback or (lambda _: None) + mesh = RhinoMesh() + + if disjoint: + vertexcolors = [] + + for face, facecolor in izip_longest(faces, facecolors or []): + f = len(face) + + if f < 3: + continue + + if f > 4: + if MeshNgon is None: + raise NotImplementedError("MeshNgons are not supported in this version of Rhino.") + + disjoint_ngon(face, vertices, mesh) + if facecolor: + for _ in range(f + 1): + vertexcolors.append(facecolor) + + else: + disjoint_face(face, vertices, mesh) + if facecolor: + for _ in range(f): + vertexcolors.append(facecolor) + + face_callback(face) + + else: + for x, y, z in vertices: + mesh.Vertices.Add(x, y, z) + + for face in faces: + f = len(face) + + if f < 3: + continue + + if f > 4: + if MeshNgon is None: + raise NotImplementedError("MeshNgons are not supported in this version of Rhino.") + + connected_ngon(face, vertices, mesh) + if vertexcolors: + vertexcolors.append(average_color([vertexcolors[index] for index in face])) + + else: + mesh.Faces.AddFace(*face) + + face_callback(face) + + # if color: + # mesh.VertexColors.CreateMonotoneMesh(SystemColor.FromArgb(*color.rgb255)) + + # else: + if not color: + if vertexcolors: + if len(mesh.Vertices) != len(vertexcolors): + raise ValueError("The number of vertex colors does not match the number of vertices.") + + colors = CreateInstance(SystemColor, len(vertexcolors)) + for index, color in enumerate(vertexcolors): + colors[index] = SystemColor.FromArgb(*color.rgb255) + + mesh.VertexColors.SetColors(colors) + + # mesh.UnifyNormals() + mesh.Normals.ComputeNormals() + mesh.Compact() + + return mesh + + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def mesh_to_compas(rhinomesh, cls=None): + """Convert a Rhino mesh object to a COMPAS mesh. + + Parameters + ---------- + rhinomesh : :class:`Rhino.Geometry.Mesh` + A Rhino mesh object. + cls: :class:`~compas.datastructures.Mesh`, optional + The mesh type. + + Returns + ------- + :class:`compas.datastructures.Mesh` + A COMPAS mesh. + + """ + cls = cls or Mesh + mesh = cls() + mesh.default_vertex_attributes.update(normal=None, color=None) + mesh.default_face_attributes.update(normal=None) + + for vertex, normal, color in zip(rhinomesh.Vertices, rhinomesh.Normals, rhinomesh.VertexColors): + mesh.add_vertex( + x=vertex.X, + y=vertex.Y, + z=vertex.Z, + normal=vector_to_compas(normal), + color=Color( + color.R, + color.G, + color.B, + ), + ) + + for face, normal in zip(rhinomesh.Faces, rhinomesh.FaceNormals): + if face.IsTriangle: + vertices = [face.A, face.B, face.C] + else: + vertices = [face.A, face.B, face.C, face.D] + mesh.add_face(vertices, normal=vector_to_compas(normal)) + + for key in rhinomesh.UserDictionary: + mesh.attributes[key] = rhinomesh.UserDictionary[key] + + return mesh diff --git a/src/compas_rhino/conversions/plane.py b/src/compas_rhino/conversions/plane.py deleted file mode 100644 index d70c7d2f7d8..00000000000 --- a/src/compas_rhino/conversions/plane.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from compas.geometry import Frame -from ._primitives import plane_to_rhino -from ._primitives import frame_to_rhino -from ._primitives import plane_to_compas -from ._primitives import plane_to_compas_frame - -from ._geometry import RhinoGeometry - - -class RhinoPlane(RhinoGeometry): - """Wrapper for Rhino planes.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Plane` | :class:`~compas.geometry.Plane` | :class:`~compas.geometry.Frame` - The geometry object defining a plane. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a plane. - - """ - if not isinstance(geometry, Rhino.Geometry.Plane): - if isinstance(geometry, Frame): - geometry = frame_to_rhino(geometry) - else: - geometry = plane_to_rhino(geometry) - self._geometry = geometry - - def to_compas(self): - """Convert to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Plane` - A COMPAS plane. - """ - return plane_to_compas(self.geometry) - - def to_compas_frame(self): - """Convert to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Frame` - A COMPAS frame. - """ - return plane_to_compas_frame(self.geometry) diff --git a/src/compas_rhino/conversions/point.py b/src/compas_rhino/conversions/point.py deleted file mode 100644 index 4a53b0f3201..00000000000 --- a/src/compas_rhino/conversions/point.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from ._primitives import point_to_rhino -from ._primitives import point_to_compas - -from .vector import RhinoVector - - -class RhinoPoint(RhinoVector): - """Wrapper for Rhino points.""" - - @property - def object(self): - return self._object - - @object.setter - def object(self, obj): - """Set the geometry of the wrapper. - - Parameters - ---------- - obj : :rhino:`Rhino_DocObjects_PointObject` - The input object. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a point. - - """ - self._guid = obj.Id - self._object = obj - self.geometry = obj.Geometry.Location - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Point3d` | :class:`~compas.geometry.Point` or list of float - The input geometry. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a plane. - - """ - if not isinstance(geometry, Rhino.Geometry.Point3d): - geometry = point_to_rhino(geometry) - self._geometry = geometry - - def to_compas(self): - """Convert the wrapper to a COMPAS point. - - Returns - ------- - :class:`~compas.geometry.Point` - A COMPAS point. - """ - return point_to_compas(self.geometry) - - def closest_point(self, point, maxdist=0.0, return_param=False): - """Compute the closest point to a point in space. - - Parameters - ---------- - point : point - A point location. - maxdist : float, optional - The maximum distance between the point on the curve and the curve. - Default is ``0.0``. - return_param : bool, optional - Return not only the point coordinates, but also the parameter of the point on the curve. - Default is False. - - Returns - ------- - tuple - The XYZ coordinates of the point. - - """ - return self.geometry.X, self.geometry.Y, self.geometry.Z diff --git a/src/compas_rhino/conversions/polyline.py b/src/compas_rhino/conversions/polyline.py deleted file mode 100644 index a79a3d99f5d..00000000000 --- a/src/compas_rhino/conversions/polyline.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from ._primitives import polyline_to_compas -from ._primitives import polyline_to_rhino - -from ._geometry import RhinoGeometry - - -class RhinoPolyline(RhinoGeometry): - """Wrapper for Rhino polylines.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Polyline` | :class:`~compas.geometry.Polyline` or list of points - The input geometry. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a polyline. - """ - if not isinstance(geometry, Rhino.Geometry.Polyline): - geometry = polyline_to_rhino(geometry) - self._geometry = geometry - - def to_compas(self): - """Convert the polyline to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Polyline` - A COMPAS polyline. - """ - return polyline_to_compas(self.geometry) diff --git a/src/compas_rhino/conversions/shapes.py b/src/compas_rhino/conversions/shapes.py new file mode 100644 index 00000000000..d031d8ff1d4 --- /dev/null +++ b/src/compas_rhino/conversions/shapes.py @@ -0,0 +1,287 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import scriptcontext as sc # type: ignore + +from Rhino.Geometry import Box as RhinoBox # type: ignore +from Rhino.Geometry import Sphere as RhinoSphere # type: ignore +from Rhino.Geometry import Cone as RhinoCone # type: ignore +from Rhino.Geometry import Cylinder as RhinoCylinder # type: ignore +from Rhino.Geometry import Torus as RhinoTorus # type: ignore +from Rhino.Geometry import Interval # type: ignore +from Rhino.Geometry import Brep as RhinoBrep # type: ignore +from Rhino.Geometry import PipeCapMode # type: ignore + +from compas.geometry import Plane +from compas.geometry import Circle +from compas.geometry import Box +from compas.geometry import Sphere +from compas.geometry import Cone +from compas.geometry import Cylinder +from compas.geometry import Torus +from compas.geometry import Frame + +# from .geometry import plane_to_rhino +from .geometry import frame_to_rhino +from .geometry import point_to_rhino +from .geometry import plane_to_compas_frame +from .geometry import plane_to_compas +from .geometry import point_to_compas +from .geometry import vector_to_compas +from .curves import line_to_rhino_curve + +from .curves import circle_to_rhino + + +# ============================================================================= +# To Rhino +# ============================================================================= + + +def box_to_rhino(box): + """Convert a COMPAS box to a Rhino box. + + Parameters + ---------- + box : :class:`~compas.geometry.Box` + + Returns + ------- + :rhino:`Rhino.Geometry.Box` + + """ + return RhinoBox( + frame_to_rhino(box.frame), + Interval(-0.5 * box.xsize, 0.5 * box.xsize), + Interval(-0.5 * box.ysize, 0.5 * box.ysize), + Interval(-0.5 * box.zsize, 0.5 * box.zsize), + ) + + +def sphere_to_rhino(sphere): + """Convert a COMPAS sphere to a Rhino sphere. + + Parameters + ---------- + sphere : :class:`~compas.geometry.Sphere` + + Returns + ------- + :rhino:`Rhino.Geometry.Sphere` + + """ + return RhinoSphere(point_to_rhino(sphere.point), sphere.radius) + + +def cone_to_rhino(cone): + """Convert a COMPAS cone to a Rhino cone. + + Parameters + ---------- + cone : :class:`~compas.geometry.Cone` + + Returns + ------- + :rhino:`Rhino.Geometry.Cone` + + """ + # return RhinoCone(plane_to_rhino(cone.circle.plane), cone.height, cone.circle.radius) + frame = Frame(cone.frame.point + cone.frame.zaxis * cone.height, cone.frame.xaxis, cone.frame.yaxis) + return RhinoCone(frame_to_rhino(frame), -cone.height, cone.radius) + + +def cone_to_rhino_brep(cone): + """Convert a COMPAS cone to a Rhino Brep. + + Parameters + ---------- + cone : :class:`~compas.geometry.Cone` + A COMPAS cone. + + Returns + ------- + Rhino.Geometry.Brep + + """ + return RhinoCone.ToBrep(cone_to_rhino(cone), True) + + +def cylinder_to_rhino(cylinder): + """Convert a COMPAS cylinder to a Rhino cylinder. + + Parameters + ---------- + cylinder : :class:`~compas.geometry.Cylinder` + + Returns + ------- + :rhino:`Rhino.Geometry.Cylinder` + + """ + circle = cylinder.circle.copy() + circle.frame.point += circle.frame.zaxis * (-0.5 * cylinder.height) + return RhinoCylinder(circle_to_rhino(circle), cylinder.height) + + +def cylinder_to_rhino_brep(cylinder): + """Convert a COMPAS cylinder to a Rhino Brep. + + Parameters + ---------- + cylinder : :class:`~compas.geometry.Cylinder` + A COMPAS cylinder. + + Returns + ------- + Rhino.Geometry.Brep + + """ + return RhinoCylinder.ToBrep(cylinder_to_rhino(cylinder), True, True) + + +def capsule_to_rhino_brep(capsule): + """Convert a COMPAS capsule to a Rhino Brep. + + Parameters + ---------- + capsule : :class:`~compas.geometry.Capsule` + A COMPAS capsule. + + Returns + ------- + list[Rhino.Geometry.Brep] + + """ + abs_tol = sc.doc.ModelAbsoluteTolerance + ang_tol = sc.doc.ModelAngleToleranceRadians + + radius = capsule.radius + line = capsule.axis + curve = line_to_rhino_curve(line) + + return RhinoBrep.CreatePipe(curve, radius, False, PipeCapMode.Round, False, abs_tol, ang_tol) + + +def torus_to_rhino(torus): + """Convert a COMPAS torus to a Rhino torus. + + Parameters + ---------- + torus : :class:`~compas.geometry.Torus` + + Returns + ------- + :rhino:`Rhino.Geometry.Torus` + + """ + return RhinoTorus(frame_to_rhino(torus.frame), torus.radius_axis, torus.radius_pipe) + + +def torus_to_rhino_brep(torus): + """Convert a COMPAS torus to a Rhino Brep. + + Parameters + ---------- + torus : :class:`~compas.geometry.Torus` + A COMPAS torus. + + Returns + ------- + Rhino.Geometry.Brep + The Rhino brep representation. + + """ + return torus_to_rhino(torus).ToNurbsSurface().ToBrep() + + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def box_to_compas(box): + """Convert a Rhino box to a COMPAS box. + + Parameters + ---------- + box : :rhino:`Rhino.Geometry.Box` + + Returns + ------- + :class:`~compas.geometry.Box` + + """ + xsize = box.X.Length + ysize = box.Y.Length + zsize = box.Z.Length + frame = plane_to_compas_frame(box.Plane) + frame.point += frame.xaxis * 0.5 * xsize + frame.point += frame.yaxis * 0.5 * ysize + frame.point += frame.zaxis * 0.5 * zsize + return Box(xsize, ysize, zsize, frame=frame) + + +def sphere_to_compas(sphere): + """Convert a Rhino sphere to a COMPAS sphere. + + Parameters + ---------- + sphere : :rhino:`Rhino.Geometry.Sphere` + + Returns + ------- + :class:`~compas.geometry.Sphere` + + """ + return Sphere(point_to_compas(sphere.Center), sphere.Radius) + + +def cone_to_compas(cone): + """Convert a Rhino cone to a COMPAS cone. + + Parameters + ---------- + cone : :rhino:`Rhino.Geometry.Cone` + + Returns + ------- + :class:`~compas.geometry.Cone` + + """ + plane = Plane(cone.BasePoint, vector_to_compas(cone.Plane.Normal).inverted()) + return Cone(Circle(plane, cone.Radius), cone.Height) + + +def cylinder_to_compas(cylinder): + """Convert a Rhino cylinder to a COMPAS cylinder. + + Parameters + ---------- + cylinder : :rhino:`Rhino.Geometry.Cylinder` + + Returns + ------- + :class:`~compas.geometry.Cylinder` + + """ + plane = plane_to_compas(cylinder.BasePlane) + height = cylinder.TotalHeight + plane.point += plane.normal * (0.5 * height) + return Cylinder(Circle(plane, cylinder.Radius), height) + + +def torus_to_compas(torus): + """Convert a Rhino torus to a COMPAS torus. + + Parameters + ---------- + torus : :rhino:`Rhino.Geometry.Torus` + + Returns + ------- + :class:`~compas.geometry.Torus` + + """ + frame = plane_to_compas_frame(torus.Plane) + return Torus(torus.MajorRadius, torus.MinorRadius, frame=frame) diff --git a/src/compas_rhino/conversions/sphere.py b/src/compas_rhino/conversions/sphere.py deleted file mode 100644 index bb772c48e77..00000000000 --- a/src/compas_rhino/conversions/sphere.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino - -from compas.geometry import Sphere - -from ._exceptions import ConversionError - -from ._shapes import sphere_to_rhino -from ._shapes import sphere_to_compas - -from ._geometry import RhinoGeometry - - -class RhinoSphere(RhinoGeometry): - """Wrapper for Rhino spheres.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Sphere` | :class:`~compas.geometry.Sphere` - The geometry object defining a sphere. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a sphere. - """ - if not isinstance(geometry, Rhino.Geometry.Sphere): - if isinstance(geometry, Rhino.Geometry.Brep): - if geometry.Faces.Count != 1: - raise ConversionError("Object brep cannot be converted to a sphere.") - face = geometry.Faces.Item[0] - if not face.IsSphere(): - raise ConversionError("Object brep cannot be converted to a sphere.") - result, geometry = face.TryGetSphere() - if not result: - raise ConversionError("Object brep cannot be converted to a sphere.") - elif isinstance(geometry, Sphere): - geometry = sphere_to_rhino(geometry) - else: - raise ConversionError("Geometry object cannot be converted to a sphere: {}".format(geometry)) - self._geometry = geometry - - def to_compas(self): - """Convert to a COMPAS geometry object. - - Returns - ------- - :class:`~compas.geometry.Sphere` - A COMPAS sphere. - """ - return sphere_to_compas(self.geometry) diff --git a/src/compas_rhino/conversions/surface.py b/src/compas_rhino/conversions/surface.py deleted file mode 100644 index 52ef2710355..00000000000 --- a/src/compas_rhino/conversions/surface.py +++ /dev/null @@ -1,487 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -import Rhino -import compas_rhino - -from compas.utilities import memoize, geometric_key -from compas.datastructures import Mesh -from compas.datastructures import meshes_join - -from ._primitives import point_to_compas - -from ._geometry import RhinoGeometry -from ._exceptions import ConversionError - - -class RhinoSurface(RhinoGeometry): - """Wrapper for Rhino surfaces.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Surface` - The geometry object defining a surface. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a surface. - - """ - if not isinstance(geometry, Rhino.Geometry.Surface): - if not isinstance(geometry, Rhino.Geometry.Brep): - raise TypeError( - "Geometry cannot be interpreted as a `Rhino.Geometry.Surface`: {}".format(type(geometry)) - ) - self._geometry = geometry - - def to_compas(self): - """Convert the surface to a COMPAS surface. - - Returns - ------- - :class:`~compas_rhino.geometry.RhinoNurbsSurface` - - Raises - ------ - :class:`ConversionError` - If the surface BRep contains more than one face. - - """ - from compas_rhino.geometry import RhinoNurbsSurface - - brep = Rhino.Geometry.Brep.TryConvertBrep(self.geometry) - if brep.Surfaces.Count > 1: - raise ConversionError("Conversion of a BRep with multiple underlying surface is currently not supported.") - for geometry in brep.Surfaces: - return RhinoNurbsSurface.from_rhino(geometry) - - def to_compas_mesh(self, cls=None, facefilter=None, cleanup=False): - """Convert the surface b-rep loops to a COMPAS mesh. - - Parameters - ---------- - cls : :class:`~compas.datastructures.Mesh`, optional - The type of COMPAS mesh. - facefilter : callable, optional - A filter for selection which Brep faces to include. - If provided, the filter should return True or False per face. - A very simple filter that includes all faces is ``def facefilter(face): return True``. - Default parameter value is None in which case all faces are included. - cleanup : bool, optional - Flag indicating to clean up the result. - Cleaning up means to remove isolated faces and unused vertices. - Default is False. - - Returns - ------- - :class:`~compas.datastructures.Mesh` - The resulting mesh. - - Examples - -------- - >>> import compas_rhino - >>> from compas_rhino.geometry import RhinoSurface - >>> from compas_rhino.artists import MeshArtist - - >>> def facefilter(face): - ... success, w, h = face.GetSurfaceSize() - ... if success: - ... if w > 10 and h > 10: - ... return True - ... return False - ... - - >>> guid = compas_rhino.select_surface() - >>> surf = RhinoSurface.from_guid(guid) - >>> mesh = surf.to_compas(facefilter=facefilter) - - >>> artist = MeshArtist(mesh, layer="Blocks") - >>> artist.clear_layer() - >>> artist.draw() - - """ - if not self.geometry.HasBrepForm: - return - brep = Rhino.Geometry.Brep.TryConvertBrep(self.geometry) - if facefilter and callable(facefilter): - brepfaces = [face for face in brep.Faces if facefilter(face)] - else: - brepfaces = brep.Faces - # vertex maps and face lists - gkey_xyz = {} - faces = [] - for face in brepfaces: - loop = face.OuterLoop - curve = loop.To3dCurve() - segments = curve.Explode() - a = segments[0].PointAtStart - b = segments[0].PointAtEnd - a_gkey = geometric_key(a) - b_gkey = geometric_key(b) - gkey_xyz[a_gkey] = a - gkey_xyz[b_gkey] = b - face = [a_gkey, b_gkey] - for segment in segments[1:-1]: - b = segment.PointAtEnd - b_gkey = geometric_key(b) - face.append(b_gkey) - gkey_xyz[b_gkey] = b - faces.append(face) - # vertices and faces - gkey_index = {gkey: index for index, gkey in enumerate(gkey_xyz)} - vertices = [list(xyz) for gkey, xyz in gkey_xyz.items()] - faces = [[gkey_index[gkey] for gkey in face] for face in faces] - # remove duplicates from vertexlist - polygons = [] - for temp in faces: - face = [] - for vertex in temp: - if vertex not in face: - face.append(vertex) - polygons.append(face) - # define mesh type - cls = cls or Mesh - # create mesh - mesh = cls.from_vertices_and_faces(vertices, polygons) - mesh.name = self.name - # remove isolated faces - if cleanup: - if mesh.number_of_faces() > 1: - for face in list(mesh.faces()): - if not mesh.face_neighbors(face): - mesh.delete_face(face) - mesh.remove_unused_vertices() - return mesh - - def to_compas_quadmesh(self, nu, nv=None, weld=False, facefilter=None, cls=None): - """Convert the surface to a COMPAS mesh. - - Parameters - ---------- - nu: int - The number of faces in the u direction. - nv: int, optional - The number of faces in the v direction. - Default is the same as the u direction. - weld: bool, optional - Weld the vertices of the mesh. - Default is False. - facefilter: callable, optional - A filter for selection which Brep faces to include. - If provided, the filter should return True or False per face. - A very simple filter that includes all faces is ``def facefilter(face): return True``. - Default parameter value is None in which case all faces are included. - cls: :class:`~compas.geometry.Mesh`, optional - The type of COMPAS mesh. - - Returns - ------- - :class:`~compas.geometry.Mesh` - """ - nv = nv or nu - cls = cls or Mesh - - if not self.geometry.HasBrepForm: - return - - brep = Rhino.Geometry.Brep.TryConvertBrep(self.geometry) - - if facefilter and callable(facefilter): - faces = [face for face in brep.Faces if facefilter(face)] - else: - faces = brep.Faces - - meshes = [] - for face in faces: - domain_u = face.Domain(0) - domain_v = face.Domain(1) - du = (domain_u[1] - domain_u[0]) / (nu) - dv = (domain_v[1] - domain_v[0]) / (nv) - - @memoize - def point_at(i, j): - return point_to_compas(face.PointAt(i, j)) - - quads = [] - for i in range(nu): - for j in range(nv): - a = point_at(domain_u[0] + (i + 0) * du, domain_v[0] + (j + 0) * dv) - b = point_at(domain_u[0] + (i + 1) * du, domain_v[0] + (j + 0) * dv) - c = point_at(domain_u[0] + (i + 1) * du, domain_v[0] + (j + 1) * dv) - d = point_at(domain_u[0] + (i + 0) * du, domain_v[0] + (j + 1) * dv) - quads.append([a, b, c, d]) - - meshes.append(cls.from_polygons(quads)) - - return meshes_join(meshes, cls=cls) - - def closest_point(self, xyz): - """Return the XYZ coordinates of the closest point on the surface from input XYZ-coordinates. - - Parameters - ---------- - xyz : list - XYZ coordinates. - - Returns - ------- - list - The XYZ coordinates of the closest point on the surface. - - """ - return compas_rhino.rs.EvaluateSurface(self.guid, *compas_rhino.rs.SurfaceClosestPoint(self.guid, xyz)) - - def closest_points(self, points): - return [self.closest_point(point) for point in points] - - # def closest_point_on_boundaries(self, xyz): - # """Return the XYZ coordinates of the closest point on the boundaries of the surface from input XYZ-coordinates. - - # Parameters - # ---------- - # xyz : list - # XYZ coordinates. - - # Returns - # ------- - # list - # The XYZ coordinates of the closest point on the boundaries of the surface. - - # """ - # from compas_rhino.geometry.curve import RhinoCurve - # borders = self.borders(type=0) - # proj_dist = {tuple(proj_xyz): distance_point_point(xyz, proj_xyz) for proj_xyz in [RhinoCurve(border).closest_point(xyz) for border in borders]} - # compas_rhino.delete_objects(borders) - # return min(proj_dist, key=proj_dist.get) - - # def closest_points_on_boundaries(self, points): - # return [self.closest_point_on_boundaries(point) for point in points] - - # def space(self, density=(10, 10)): - # """Construct a parameter grid overt the UV space of the surface. - - # Parameters - # ---------- - # density : tuple, optional - # The density in the U and V directions of the parameter space. - # Default is ``10`` in both directions. - - # Returns - # ------- - # list - # A list of UV parameter tuples. - # """ - # rs = compas_rhino.rs - # rs.EnableRedraw(False) - # try: - # du, dv = density - # except TypeError: - # du = density - # dv = density - # density_u = int(du) - # density_v = int(dv) - # if rs.IsPolysurface(self.guid): - # faces = rs.ExplodePolysurfaces(self.guid) - # elif rs.IsSurface(self.guid): - # faces = [self.guid] - # else: - # raise Exception('Object is not a (poly)surface.') - # uv = [] - # for face in faces: - # domain_u = rs.SurfaceDomain(face, 0) - # domain_v = rs.SurfaceDomain(face, 1) - # du = (domain_u[1] - domain_u[0]) / (density_u - 1) - # dv = (domain_v[1] - domain_v[0]) / (density_v - 1) - # for i, j in product(range(density_u), range(density_v)): - # uv.append((domain_u[0] + i * du, domain_v[0] + j * dv)) - # if len(faces) > 1: - # rs.DeleteObjects(faces) - # rs.EnableRedraw(True) - # return uv - - # def heightfield(self, density=(10, 10), over_space=True): - # """Construct a point grid over the surface. - - # Parameters - # ---------- - # density : tuple, optional - # The density in the U and V directions of the grid. - # Default is ``10`` in both directions. - # over_space : bool, optional - # Construct the grid over the UV space of the surface. - # Default is True. - - # Returns - # ------- - # list - # List of grid points. - - # """ - # rs = compas_rhino.rs - # rs.EnableRedraw(False) - # try: - # du, dv = density - # except TypeError: - # du = density - # dv = density - # du = int(du) - # dv = int(dv) - # if rs.IsPolysurface(self.guid): - # faces = rs.ExplodePolysurfaces(self.guid) - # elif rs.IsSurface(self.guid): - # faces = [self.guid] - # else: - # raise Exception('Object is not a surface.') - # xyz = [] - # if over_space: - # for guid in faces: - # face = RhinoSurface.from_guid(guid) - # uv = face.space(density) - # for u, v in uv: - # xyz.append(list(rs.EvaluateSurface(face.guid, u, v))) - # else: - # for guid in faces: - # bbox = rs.BoundingBox(guid) - # xmin = bbox[0][0] - # xmax = bbox[1][0] - # ymin = bbox[0][1] - # ymax = bbox[3][1] - # xstep = 1.0 * (xmax - xmin) / (du - 1) - # ystep = 1.0 * (ymax - ymin) / (dv - 1) - # seeds = [] - # for i in range(du): - # for j in range(dv): - # seed = xmin + i * xstep, ymin + j * ystep, 0 - # seeds.append(seed) - # points = map(list, rs.ProjectPointToSurface(seeds, guid, [0, 0, 1])) - # xyz += points - # if len(faces) > 1: - # rs.DeleteObjects(faces) - # rs.EnableRedraw(True) - # return xyz - - # def descent(self, points=None): - # """""" - # rs = compas_rhino.rs - # if not points: - # points = self.heightfield() - # tol = rs.UnitAbsoluteTolerance() - # descent = [] - # if rs.IsPolysurface(self.guid): - # rs.EnableRedraw(False) - # faces = {} - # for p0 in points: - # p = p0[:] - # p[2] -= 2 * tol - # bcp = rs.BrepClosestPoint(self.guid, p) - # uv = bcp[1] - # index = bcp[2][1] - # try: - # face = faces[index] - # except (TypeError, IndexError): - # face = rs.ExtractSurface(self.guid, index, True) - # faces[index] = face - # p1 = rs.EvaluateSurface(face, uv[0], uv[1]) - # vector = [p1[_] - p0[_] for _ in range(3)] - # descent.append((p0, vector)) - # rs.DeleteObjects(faces.values()) - # rs.EnableRedraw(True) - # elif rs.IsSurface(self.guid): - # for p0 in points: - # p = p0[:] - # p[2] -= 2 * tol - # bcp = rs.BrepClosestPoint(self.guid, p) - # uv = bcp[1] - # p1 = rs.EvaluateSurface(self.guid, uv[0], uv[1]) - # vector = [p1[_] - p0[_] for _ in range(3)] - # descent.append((p0, vector)) - # else: - # raise Exception('Object is not a surface.') - # return descent - - # def curvature(self, points=None): - # """""" - # rs = compas_rhino.rs - # if not points: - # points = self.heightfield() - # curvature = [] - # if rs.IsPolysurface(self.guid): - # rs.EnableRedraw(False) - # faces = {} - # for point in points: - # bcp = rs.BrepClosestPoint(self.guid, point) - # uv = bcp[1] - # index = bcp[2][1] - # try: - # face = faces[index] - # except (TypeError, IndexError): - # face = rs.ExtractSurface(self.guid, index, True) - # faces[index] = face - # props = rs.SurfaceCurvature(face, uv) - # curvature.append((point, (props[1], props[3], props[5]))) - # rs.DeleteObjects(faces.values()) - # rs.EnableRedraw(False) - # elif rs.IsSurface(self.guid): - # for point in points: - # bcp = rs.BrepClosestPoint(self.guid, point) - # uv = bcp[1] - # props = rs.SurfaceCurvature(self.guid, uv) - # curvature.append((point, (props[1], props[3], props[5]))) - # else: - # raise Exception('Object is not a surface.') - # return curvature - - # def borders(self, border_type=1): - # """Duplicate the borders of the surface. - - # Parameters - # ---------- - # border_type : {0, 1, 2} - # The type of border. - - # * 0: All borders - # * 1: The exterior borders. - # * 2: The interior borders. - - # Returns - # ------- - # list - # The GUIDs of the extracted border curves. - # """ - # rs = compas_rhino.rs - # border = rs.DuplicateSurfaceBorder(self.guid, type=border_type) - # curves = rs.ExplodeCurves(border, delete_input=True) - # return curves - - # def kinks(self, threshold=1e-3): - # """Return the XYZ coordinates of kinks, i.e. tangency discontinuities, along the surface's boundaries. - - # Returns - # ------- - # list - # The list of XYZ coordinates of surface boundary kinks. - # """ - # from compas_rhino.geometry.curve import RhinoCurve - # rs = compas_rhino.rs - # kinks = [] - # borders = self.borders(border_type=0) - # for border in borders: - # border = RhinoCurve(border) - # extremities = map(lambda x: rs.EvaluateCurve(border.guid, rs.CurveParameter(border.guid, x)), [0., 1.]) - # if border.is_closed(): - # start_tgt, end_tgt = border.tangents(extremities) - # if angle_vectors(start_tgt, end_tgt) > threshold: - # kinks += extremities - # else: - # kinks += extremities - # return list(set(kinks)) diff --git a/src/compas_rhino/conversions/surfaces.py b/src/compas_rhino/conversions/surfaces.py new file mode 100644 index 00000000000..592bc326a73 --- /dev/null +++ b/src/compas_rhino/conversions/surfaces.py @@ -0,0 +1,335 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.geometry import Point +from compas.datastructures import Mesh +from compas.datastructures import meshes_join +from compas.utilities import geometric_key +from compas.utilities import memoize + +from Rhino.Geometry import NurbsSurface as RhinoNurbsSurface # type: ignore +from Rhino.Geometry import Brep as RhinoBrep # type: ignore + +from .exceptions import ConversionError +from .geometry import point_to_rhino +from .geometry import point_to_compas + + +# ============================================================================= +# To Rhino +# ============================================================================= + + +def surface_to_rhino(surface): + """Convert a COMPAS surface to a Rhino surface. + + Parameters + ---------- + surface : :class:`~compas.geometry.Surface` + A COMPAS surface. + + Returns + ------- + Rhino.Geometry.Surface + + """ + return surface.rhino_surface + + +def data_to_rhino_surface(data): + """Convert a COMPAS surface to a Rhino surface. + + Parameters + ---------- + data: dict + + Returns + ------- + :rhino:`Rhino.Geometry.NurbsSurface` + + """ + points = [[Point.from_data(point) for point in row] for row in data["points"]] + + nu = len(points[0]) + nv = len(points) + + nurbs = RhinoNurbsSurface.Create(3, False, data["u_degree"] + 1, data["v_degree"] + 1, nu, nv) + for i in range(nu): + for j in range(nv): + nurbs.Points.SetPoint(i, j, point_to_rhino(points[j][i])) + nurbs.Points.SetWeight(i, j, data["weights"][j][i]) + + u_knotvector = [] + for knot, mult in zip(data["u_knots"], data["u_mults"]): + for i in range(mult): + u_knotvector.append(knot) + + for index, knot in enumerate(u_knotvector): + nurbs.KnotsU.Item[index] = knot + + v_knotvector = [] + for knot, mult in zip(data["v_knots"], data["v_mults"]): + for i in range(mult): + v_knotvector.append(knot) + + for index, knot in enumerate(v_knotvector): + nurbs.KnotsV.Item[index] = knot + + return nurbs + + +# ============================================================================= +# To COMPAS +# ============================================================================= + + +def surface_to_compas_data(surface): + """Convert a Rhino surface to a COMPAS surface. + + Parameters + ---------- + surface: :rhino:`Rhino.Geometry.Surface` + + Returns + ------- + dict + + """ + surface = surface.ToNurbsSurface() + + points = [] + weights = [] + for j in range(surface.Points.VCount): + _points = [] + _weights = [] + for i in range(surface.Points.UCount): + point = surface.Points.GetPoint(i, j) + weight = surface.Points.GetWeight(i, j) + _points.append(point_to_compas(point)) + _weights.append(weight) + points.append(_points) + weights.append(_weights) + + u_knots = [] + u_mults = [] + for index in range(surface.KnotsU.Count): + u_knots.append(surface.KnotsU.Item[index]) + u_mults.append(surface.KnotsU.KnotMultiplicity(index)) + + v_knots = [] + v_mults = [] + for index in range(surface.KnotsV.Count): + v_knots.append(surface.KnotsV.Item[index]) + v_mults.append(surface.KnotsV.KnotMultiplicity(index)) + + u_degree = surface.OrderU - 1 + v_degree = surface.OrderV - 1 + + is_u_periodic = False + is_v_periodic = False + + return { + "points": [[point.data for point in row] for row in points], + "weights": weights, + "u_knots": u_knots, + "v_knots": v_knots, + "u_mults": u_mults, + "v_mults": v_mults, + "u_degree": u_degree, + "v_degree": v_degree, + "is_u_periodic": is_u_periodic, + "is_v_periodic": is_v_periodic, + } + + +def surface_to_compas(surface): + """Convert a Rhino surface to a COMPAS surface. + + Parameters + ---------- + surface: :rhino:`Rhino.Geometry.Surface` + + Returns + ------- + :class:`~compas.geometry.Surface` + + """ + from compas_rhino.geometry import RhinoNurbsSurface + + brep = RhinoBrep.TryConvertBrep(surface) + + if brep.Surfaces.Count > 1: # type: ignore + raise ConversionError("Conversion of a BRep with multiple underlying surface is currently not supported.") + + return RhinoNurbsSurface.from_rhino(brep.Surfaces[0]) + + +def surface_to_compas_mesh(surface, cls=None, facefilter=None, cleanup=False): + """Convert the surface b-rep loops to a COMPAS mesh. + + Parameters + ---------- + cls : :class:`~compas.datastructures.Mesh`, optional + The type of COMPAS mesh. + facefilter : callable, optional + A filter for selection which Brep faces to include. + If provided, the filter should return True or False per face. + A very simple filter that includes all faces is ``def facefilter(face): return True``. + Default parameter value is None in which case all faces are included. + cleanup : bool, optional + Flag indicating to clean up the result. + Cleaning up means to remove isolated faces and unused vertices. + Default is False. + + Returns + ------- + :class:`~compas.datastructures.Mesh` + The resulting mesh. + + Examples + -------- + >>> import compas_rhino + >>> from compas_rhino.geometry import RhinoSurface + >>> from compas_rhino.artists import MeshArtist + + >>> def facefilter(face): + ... success, w, h = face.GetSurfaceSize() + ... if success: + ... if w > 10 and h > 10: + ... return True + ... return False + ... + + >>> guid = compas_rhino.select_surface() + >>> surf = RhinoSurface.from_guid(guid) + >>> mesh = surf.to_compas(facefilter=facefilter) + + >>> artist = MeshArtist(mesh, layer="Blocks") + >>> artist.clear_layer() + >>> artist.draw() + + """ + if not surface.HasBrepForm: + return + + brep = RhinoBrep.TryConvertBrep(surface) + + if facefilter and callable(facefilter): + brepfaces = [face for face in brep.Faces if facefilter(face)] + else: + brepfaces = brep.Faces + + # vertex maps and face lists + gkey_xyz = {} + faces = [] + for face in brepfaces: + loop = face.OuterLoop + curve = loop.To3dCurve() + segments = curve.Explode() + a = segments[0].PointAtStart + b = segments[0].PointAtEnd + a_gkey = geometric_key(a) + b_gkey = geometric_key(b) + gkey_xyz[a_gkey] = a + gkey_xyz[b_gkey] = b + face = [a_gkey, b_gkey] + for segment in segments[1:-1]: + b = segment.PointAtEnd + b_gkey = geometric_key(b) + face.append(b_gkey) + gkey_xyz[b_gkey] = b + faces.append(face) + + # vertices and faces + gkey_index = {gkey: index for index, gkey in enumerate(gkey_xyz)} + vertices = [list(xyz) for gkey, xyz in gkey_xyz.items()] + faces = [[gkey_index[gkey] for gkey in face] for face in faces] + + # remove duplicates from vertexlist + polygons = [] + for temp in faces: + face = [] + for vertex in temp: + if vertex not in face: + face.append(vertex) + polygons.append(face) + + # define mesh type + cls = cls or Mesh + # create mesh + mesh = cls.from_vertices_and_faces(vertices, polygons) + + # remove isolated faces + if cleanup: + if mesh.number_of_faces() > 1: + for face in list(mesh.faces()): + if not mesh.face_neighbors(face): + mesh.delete_face(face) + mesh.remove_unused_vertices() + + return mesh + + +def surface_to_compas_quadmesh(surface, nu, nv=None, weld=False, facefilter=None, cls=None): + """Convert the surface to a COMPAS mesh. + + Parameters + ---------- + nu: int + The number of faces in the u direction. + nv: int, optional + The number of faces in the v direction. + Default is the same as the u direction. + weld: bool, optional + Weld the vertices of the mesh. + Default is False. + facefilter: callable, optional + A filter for selection which Brep faces to include. + If provided, the filter should return True or False per face. + A very simple filter that includes all faces is ``def facefilter(face): return True``. + Default parameter value is None in which case all faces are included. + cls: :class:`~compas.geometry.Mesh`, optional + The type of COMPAS mesh. + + Returns + ------- + :class:`~compas.geometry.Mesh` + + """ + nv = nv or nu + cls = cls or Mesh + + if not surface.HasBrepForm: + return + + brep = RhinoBrep.TryConvertBrep(surface) + + if facefilter and callable(facefilter): + faces = [face for face in brep.Faces if facefilter(face)] + else: + faces = brep.Faces + + meshes = [] + for face in faces: + domain_u = face.Domain(0) + domain_v = face.Domain(1) + du = (domain_u[1] - domain_u[0]) / (nu) + dv = (domain_v[1] - domain_v[0]) / (nv) + + @memoize + def point_at(i, j): + return point_to_compas(face.PointAt(i, j)) + + quads = [] + for i in range(nu): + for j in range(nv): + a = point_at(domain_u[0] + (i + 0) * du, domain_v[0] + (j + 0) * dv) + b = point_at(domain_u[0] + (i + 1) * du, domain_v[0] + (j + 0) * dv) + c = point_at(domain_u[0] + (i + 1) * du, domain_v[0] + (j + 1) * dv) + d = point_at(domain_u[0] + (i + 0) * du, domain_v[0] + (j + 1) * dv) + quads.append([a, b, c, d]) + + meshes.append(cls.from_polygons(quads)) + + return meshes_join(meshes, cls=cls) diff --git a/src/compas_rhino/conversions/transformations.py b/src/compas_rhino/conversions/transformations.py new file mode 100644 index 00000000000..c9ea60a791c --- /dev/null +++ b/src/compas_rhino/conversions/transformations.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from Rhino.Geometry import Transform # type: ignore + + +def transformation_to_rhino(transformation): + """Creates a Rhino transformation from a COMPAS transformation. + + Parameters + ---------- + transformation : :class:`~compas.geometry.Transformation` + COMPAS transformation. + + Returns + ------- + :rhino:`Rhino.Geometry.Transform` + + """ + transform = Transform(1.0) + for i in range(0, 4): + for j in range(0, 4): + transform[i, j] = transformation[i, j] + return transform + + +def transformation_matrix_to_rhino(matrix): + """Creates a Rhino transformation from a 4x4 transformation matrix. + + Parameters + ---------- + matrix : list[list[float]] + The 4x4 transformation matrix in row-major order. + + Returns + ------- + :rhino:`Rhino.Geometry.Transform` + + """ + transform = Transform(1.0) + for i in range(0, 4): + for j in range(0, 4): + transform[i, j] = matrix[i][j] + return transform diff --git a/src/compas_rhino/conversions/vector.py b/src/compas_rhino/conversions/vector.py deleted file mode 100644 index 7cea91cc567..00000000000 --- a/src/compas_rhino/conversions/vector.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import Rhino - -from ._primitives import vector_to_rhino -from ._primitives import vector_to_compas - -from ._geometry import RhinoGeometry - - -class RhinoVector(RhinoGeometry): - """Wrapper for Rhino vectors.""" - - @property - def geometry(self): - return self._geometry - - @geometry.setter - def geometry(self, geometry): - """Set the geometry of the wrapper. - - Parameters - ---------- - geometry : :rhino:`Rhino_Geometry_Vector3d` | :class:`~compas.geometry.Vector` or list of float - The input geometry. - - Raises - ------ - :class:`ConversionError` - If the geometry cannot be converted to a vector. - """ - if not isinstance(geometry, Rhino.Geometry.Vector3d): - geometry = vector_to_rhino(geometry) - self._geometry = geometry - - def to_compas(self): - """Convert the wrapper to a COMPAS object. - - Returns - ------- - :class:`~compas.geometry.Vector` - A COMPAS vector. - """ - return vector_to_compas(self.geometry) diff --git a/src/compas_rhino/geometry/curves/curve.py b/src/compas_rhino/geometry/curves/curve.py index d513c47e0ef..8c2bd3d6c96 100644 --- a/src/compas_rhino/geometry/curves/curve.py +++ b/src/compas_rhino/geometry/curves/curve.py @@ -8,7 +8,7 @@ from compas_rhino.conversions import point_to_rhino from compas_rhino.conversions import point_to_compas from compas_rhino.conversions import vector_to_compas -from compas_rhino.conversions import xform_to_rhino +from compas_rhino.conversions import transformation_to_rhino from compas_rhino.conversions import plane_to_compas_frame from compas_rhino.conversions import plane_to_rhino from compas_rhino.conversions import box_to_compas @@ -49,7 +49,7 @@ def __init__(self, name=None): self._rhino_curve = None def __eq__(self, other): - return self.rhino_curve.IsEqual(other.rhino_curve) + return self.rhino_curve.IsEqual(other.rhino_curve) # type: ignore # ============================================================================== # Data @@ -136,7 +136,7 @@ def copy(self): """ cls = type(self) curve = cls() - curve.rhino_curve = self.rhino_curve.Duplicate() + curve.rhino_curve = self.rhino_curve.Duplicate() # type: ignore return curve def transform(self, T): @@ -152,7 +152,7 @@ def transform(self, T): None """ - self.rhino_curve.Transform(xform_to_rhino(T)) + self.rhino_curve.Transform(transformation_to_rhino(T)) # type: ignore def reverse(self): """Reverse the parametrisation of the curve. @@ -162,7 +162,7 @@ def reverse(self): None """ - self.rhino_curve.Reverse() + self.rhino_curve.Reverse() # type: ignore def point_at(self, t): """Compute a point on the curve. @@ -178,7 +178,7 @@ def point_at(self, t): the corresponding point on the curve. """ - point = self.rhino_curve.PointAt(t) + point = self.rhino_curve.PointAt(t) # type: ignore return point_to_compas(point) def tangent_at(self, t): @@ -195,7 +195,7 @@ def tangent_at(self, t): The corresponding tangent vector. """ - vector = self.rhino_curve.TangentAt(t) + vector = self.rhino_curve.TangentAt(t) # type: ignore return vector_to_compas(vector) def curvature_at(self, t): @@ -212,7 +212,7 @@ def curvature_at(self, t): The corresponding curvature vector. """ - vector = self.rhino_curve.CurvatureAt(t) + vector = self.rhino_curve.CurvatureAt(t) # type: ignore return vector_to_compas(vector) def frame_at(self, t): @@ -229,7 +229,7 @@ def frame_at(self, t): The corresponding local frame. """ - t, plane = self.rhino_curve.FrameAt(t) + t, plane = self.rhino_curve.FrameAt(t) # type: ignore return plane_to_compas_frame(plane) def torsion_at(self, t): @@ -246,7 +246,7 @@ def torsion_at(self, t): The torsion value. """ - return self.rhino_curve.TorsionAt(t) + return self.rhino_curve.TorsionAt(t) # type: ignore # ============================================================================== # Methods continued @@ -269,7 +269,7 @@ def closest_point(self, point, return_parameter=False): If `return_parameter` is True, the closest point and the corresponding parameter are returned. """ - result, t = self.rhino_curve.ClosestPoint(point_to_rhino(point)) + result, t = self.rhino_curve.ClosestPoint(point_to_rhino(point)) # type: ignore if not result: return point = self.point_at(t) @@ -296,7 +296,7 @@ def divide_by_count(self, count, return_points=False): If `return_points` is True, a list of points in addition to the parameters of the discretisation. """ - params = self.rhino_curve.DivideByCount(count, True) + params = self.rhino_curve.DivideByCount(count, True) # type: ignore if return_points: points = [self.point_at(t) for t in params] return params, points @@ -321,7 +321,7 @@ def divide_by_length(self, length, return_points=False): If `return_points` is True, a list of points in addition to the parameters of the discretisation. """ - params = self.rhino_curve.DivideByLength(length, True) + params = self.rhino_curve.DivideByLength(length, True) # type: ignore if return_points: points = [self.point_at(t) for t in params] return params, points @@ -335,7 +335,7 @@ def aabb(self): :class:`~compas.geometry.Box` """ - box = self.rhino_curve.getBoundingBox(True) + box = self.rhino_curve.getBoundingBox(True) # type: ignore return box_to_compas(box) def length(self, precision=1e-8): @@ -347,7 +347,7 @@ def length(self, precision=1e-8): Required precision of the calculated length. """ - return self.rhino_curve.GetLength(precision) + return self.rhino_curve.GetLength(precision) # type: ignore def fair(self, tol=1e-3): raise NotImplementedError @@ -368,10 +368,10 @@ def offset(self, distance, direction, tolerance=1e-3): None """ - point = self.point_at(self.domain[0]) + point = self.point_at(self.domain[0]) # type: ignore plane = Plane(point, direction) plane = plane_to_rhino(plane) - self.rhino_curve = self.rhino_curve.Offset(plane, distance, tolerance, 0)[0] + self.rhino_curve = self.rhino_curve.Offset(plane, distance, tolerance, 0)[0] # type: ignore def smooth(self): raise NotImplementedError diff --git a/src/compas_rhino/geometry/curves/nurbs.py b/src/compas_rhino/geometry/curves/nurbs.py index 3aa4f09ef1e..a899ab17dcb 100644 --- a/src/compas_rhino/geometry/curves/nurbs.py +++ b/src/compas_rhino/geometry/curves/nurbs.py @@ -12,7 +12,7 @@ from .curve import RhinoCurve -import Rhino.Geometry +import Rhino.Geometry # type: ignore def rhino_curve_from_parameters(points, weights, knots, multiplicities, degree): @@ -82,11 +82,11 @@ def data(self): # add superfluous knots # for compatibility with all/most other NURBS implementations # https://developer.rhino3d.com/guides/opennurbs/superfluous-knots/ - multiplicities = self.multiplicities[:] + multiplicities = self.multiplicities[:] # type: ignore multiplicities[0] += 1 multiplicities[-1] += 1 return { - "points": [point.data for point in self.points], + "points": [point.data for point in self.points], # type: ignore "weights": self.weights, "knots": self.knots, "multiplicities": multiplicities, diff --git a/src/compas_rhino/geometry/surfaces/nurbs.py b/src/compas_rhino/geometry/surfaces/nurbs.py index 8b2f82261fe..0e8e214cdc2 100644 --- a/src/compas_rhino/geometry/surfaces/nurbs.py +++ b/src/compas_rhino/geometry/surfaces/nurbs.py @@ -6,13 +6,15 @@ from compas.geometry import Point from compas.geometry import NurbsSurface +from compas.geometry import knots_and_mults_to_knotvector +from compas.utilities import flatten from compas_rhino.conversions import point_to_rhino from compas_rhino.conversions import point_to_compas from .surface import RhinoSurface -import Rhino.Geometry +import Rhino.Geometry # type: ignore class ControlPoints(object): @@ -52,47 +54,58 @@ def __iter__(self): def rhino_surface_from_parameters( points, weights, - u_knots, - v_knots, - u_mults, - v_mults, - u_degree, - v_degree, - is_u_periodic=False, - is_v_periodic=False, + knots_u, + knots_v, + mults_u, + mults_v, + degree_u, + degree_v, + is_periodic_u=False, + is_periodic_v=False, ): - u_order = u_degree + 1 - v_order = v_degree + 1 - u_point_count = len(points) - v_point_count = len(points[0]) - is_rational = True # TODO: check if all weights are equal? https://developer.rhino3d.com/guides/opennurbs/nurbs-geometry-overview/ + order_u = degree_u + 1 + order_v = degree_v + 1 + pointcount_u = len(points) + pointcount_v = len(points[0]) + is_rational = any(weight != 1.0 for weight in flatten(weights)) dimensions = 3 + rhino_surface = Rhino.Geometry.NurbsSurface.Create( - dimensions, is_rational, u_order, v_order, u_point_count, v_point_count + dimensions, + is_rational, + order_u, + order_v, + pointcount_u, + pointcount_v, ) if not rhino_surface: - message = "dimensions: {} is_rational: {} u_order: {} v_order: {} u_points: {} v_points: {}".format( - dimensions, is_rational, u_order, v_order, u_point_count, v_point_count + message = "dimensions: {} is_rational: {} order_u: {} order_v: {} u_points: {} v_points: {}".format( + dimensions, + is_rational, + order_u, + order_v, + pointcount_u, + pointcount_v, ) raise ValueError("Failed to create NurbsSurface with params:\n{}".format(message)) - u_knotvector = [knot for knot, mult in zip(u_knots, u_mults) for _ in range(mult)] - v_knotvector = [knot for knot, mult in zip(v_knots, v_mults) for _ in range(mult)] + knotvector_u = knots_and_mults_to_knotvector(knots_u, mults_u) + knotvector_v = knots_and_mults_to_knotvector(knots_v, mults_v) # account for superfluous knots # https://developer.rhino3d.com/guides/opennurbs/superfluous-knots/ - if len(u_knotvector) == u_point_count + u_order: - u_knotvector[:] = u_knotvector[1:-1] - if len(v_knotvector) == v_point_count + v_order: - v_knotvector[:] = v_knotvector[1:-1] + if len(knotvector_u) == pointcount_u + order_u: + knotvector_u[:] = knotvector_u[1:-1] + if len(knotvector_v) == pointcount_v + order_v: + knotvector_v[:] = knotvector_v[1:-1] # add knots - for index, knot in enumerate(u_knotvector): + for index, knot in enumerate(knotvector_u): rhino_surface.KnotsU[index] = knot - for index, knot in enumerate(v_knotvector): + for index, knot in enumerate(knotvector_v): rhino_surface.KnotsV[index] = knot # add control points - for i in range(u_point_count): - for j in range(v_point_count): + for i in range(pointcount_u): + for j in range(pointcount_v): rhino_surface.Points.SetPoint(i, j, point_to_rhino(points[i][j]), weights[i][j]) return rhino_surface @@ -106,17 +119,17 @@ class RhinoNurbsSurface(RhinoSurface, NurbsSurface): The control points of the surface. weights: list[list[float]] The weights of the control points. - u_knots: list[float] + knots_u: list[float] The knot vector, in the U direction, without duplicates. - v_knots: list[float] + knots_v: list[float] The knot vector, in the V direction, without duplicates. - u_mults: list[int] + mults_u: list[int] The multiplicities of the knots in the knot vector of the U direction. - v_mults: list[int] + mults_v: list[int] The multiplicities of the knots in the knot vector of the V direction. - u_degree: int + degree_u: int The degree of the polynomials in the U direction. - v_degree: int + degree_v: int The degree of the polynomials in the V direction. """ @@ -134,48 +147,48 @@ def data(self): # add superfluous knots # for compatibility with all/most other NURBS implementations # https://developer.rhino3d.com/guides/opennurbs/superfluous-knots/ - u_mults = self.u_mults[:] - v_mults = self.v_mults[:] - u_mults[0] += 1 - u_mults[-1] += 1 - v_mults[0] += 1 - v_mults[-1] += 1 + mults_u = self.mults_u[:] # type: ignore + mults_v = self.mults_v[:] # type: ignore + mults_u[0] += 1 + mults_u[-1] += 1 + mults_v[0] += 1 + mults_v[-1] += 1 return { - "points": [[point.data for point in row] for row in self.points], + "points": [[point.data for point in row] for row in self.points], # type: ignore "weights": self.weights, - "u_knots": self.u_knots, - "v_knots": self.v_knots, - "u_mults": u_mults, - "v_mults": v_mults, - "u_degree": self.u_degree, - "v_degree": self.v_degree, - "is_u_periodic": self.is_u_periodic, - "is_v_periodic": self.is_v_periodic, + "knots_u": self.knots_u, + "knots_v": self.knots_v, + "mults_u": mults_u, + "mults_v": mults_v, + "degree_u": self.degree_u, + "degree_v": self.degree_v, + "is_periodic_u": self.is_periodic_u, + "is_periodic_v": self.is_periodic_v, } @data.setter def data(self, data): points = [[Point.from_data(point) for point in row] for row in data["points"]] weights = data["weights"] - u_knots = data["u_knots"] - v_knots = data["v_knots"] - u_mults = data["u_mults"] - v_mults = data["v_mults"] - u_degree = data["u_degree"] - v_degree = data["v_degree"] - is_u_periodic = data["is_u_periodic"] - is_v_periodic = data["is_v_periodic"] + knots_u = data["knots_u"] + knots_v = data["knots_v"] + mults_u = data["mults_u"] + mults_v = data["mults_v"] + degree_u = data["degree_u"] + degree_v = data["degree_v"] + is_periodic_u = data["is_periodic_u"] + is_periodic_v = data["is_periodic_v"] self.rhino_surface = NurbsSurface.from_parameters( points, weights, - u_knots, - v_knots, - u_mults, - v_mults, - u_degree, - v_degree, - is_u_periodic, - is_v_periodic, + knots_u, + knots_v, + mults_u, + mults_v, + degree_u, + degree_v, + is_periodic_u, + is_periodic_v, ) @classmethod @@ -195,25 +208,25 @@ def from_data(cls, data): """ points = [[Point.from_data(point) for point in row] for row in data["points"]] weights = data["weights"] - u_knots = data["u_knots"] - v_knots = data["v_knots"] - u_mults = data["u_mults"] - v_mults = data["v_mults"] - u_degree = data["u_degree"] - v_degree = data["v_degree"] - is_u_periodic = data["is_u_periodic"] - is_v_periodic = data["is_v_periodic"] + knots_u = data["knots_u"] + knots_v = data["knots_v"] + mults_u = data["mults_u"] + mults_v = data["mults_v"] + degree_u = data["degree_u"] + degree_v = data["degree_v"] + is_periodic_u = data["is_periodic_u"] + is_periodic_v = data["is_periodic_v"] return cls.from_parameters( points, weights, - u_knots, - v_knots, - u_mults, - v_mults, - u_degree, - v_degree, - is_u_periodic, - is_v_periodic, + knots_u, + knots_v, + mults_u, + mults_v, + degree_u, + degree_v, + is_periodic_u, + is_periodic_v, ) # ============================================================================== @@ -239,42 +252,42 @@ def weights(self): return weights @property - def u_knots(self): + def knots_u(self): if self.rhino_surface: return [key for key, _ in groupby(self.rhino_surface.KnotsU)] @property - def u_knotsequence(self): + def mults_u(self): if self.rhino_surface: - return list(self.rhino_surface.KnotsU) + return [len(list(group)) for _, group in groupby(self.rhino_surface.KnotsU)] @property - def v_knots(self): + def knotvector_u(self): if self.rhino_surface: - return [key for key, _ in groupby(self.rhino_surface.KnotsV)] + return list(self.rhino_surface.KnotsU) @property - def v_knotsequence(self): + def knots_v(self): if self.rhino_surface: - return list(self.rhino_surface.KnotsV) + return [key for key, _ in groupby(self.rhino_surface.KnotsV)] @property - def u_mults(self): + def mults_v(self): if self.rhino_surface: - return [len(list(group)) for _, group in groupby(self.rhino_surface.KnotsU)] + return [len(list(group)) for _, group in groupby(self.rhino_surface.KnotsV)] @property - def v_mults(self): + def knotvector_v(self): if self.rhino_surface: - return [len(list(group)) for _, group in groupby(self.rhino_surface.KnotsV)] + return list(self.rhino_surface.KnotsV) @property - def u_degree(self): + def degree_u(self): if self.rhino_surface: return self.rhino_surface.Degree(0) @property - def v_degree(self): + def degree_v(self): if self.rhino_surface: return self.rhino_surface.Degree(1) @@ -287,14 +300,14 @@ def from_parameters( cls, points, weights, - u_knots, - v_knots, - u_mults, - v_mults, - u_degree, - v_degree, - is_u_periodic=False, - is_v_periodic=False, + knots_u, + knots_v, + mults_u, + mults_v, + degree_u, + degree_v, + is_periodic_u=False, + is_periodic_v=False, ): """Construct a NURBS surface from explicit parameters. @@ -304,17 +317,17 @@ def from_parameters( The control points. weights : list[list[float]] The weights of the control points. - u_knots : list[float] + knots_u : list[float] The knots in the U direction, without multiplicity. - v_knots : list[float] + knots_v : list[float] The knots in the V direction, without multiplicity. - u_mults : list[int] + mults_u : list[int] Multiplicity of the knots in the U direction. - v_mults : list[int] + mults_v : list[int] Multiplicity of the knots in the V direction. - u_degree : int + degree_u : int Degree in the U direction. - v_degree : int + degree_v : int Degree in the V direction. Returns @@ -324,21 +337,21 @@ def from_parameters( """ surface = cls() surface.rhino_surface = rhino_surface_from_parameters( - points, weights, u_knots, v_knots, u_mults, v_mults, u_degree, v_degree + points, weights, knots_u, knots_v, mults_u, mults_v, degree_u, degree_v ) return surface @classmethod - def from_points(cls, points, u_degree=3, v_degree=3): + def from_points(cls, points, degree_u=3, degree_v=3): """Construct a NURBS surface from control points. Parameters ---------- points : list[list[:class:`~compas.geometry.Point`]] The control points. - u_degree : int + degree_u : int Degree in the U direction. - v_degree : int + degree_v : int Degree in the V direction. Returns @@ -346,13 +359,16 @@ def from_points(cls, points, u_degree=3, v_degree=3): :class:`~compas_rhino.geometry.RhinoNurbsSurface` """ + # this of course depends on the order in which the points are given. + # with the current convention this should not be needed. points = list(zip(*points)) - u_count = len(points[0]) - v_count = len(points) + + pointcount_u = len(points) + pointcount_v = len(points[0]) points[:] = [point_to_rhino(point) for row in points for point in row] surface = cls() surface.rhino_surface = Rhino.Geometry.NurbsSurface.CreateFromPoints( - points, v_count, u_count, u_degree, v_degree + points, pointcount_u, pointcount_v, degree_u, degree_v ) return surface diff --git a/src/compas_rhino/geometry/surfaces/surface.py b/src/compas_rhino/geometry/surfaces/surface.py index ab5b13de229..701988fbdd0 100644 --- a/src/compas_rhino/geometry/surfaces/surface.py +++ b/src/compas_rhino/geometry/surfaces/surface.py @@ -11,13 +11,13 @@ from compas_rhino.conversions import frame_to_rhino_plane from compas_rhino.conversions import plane_to_rhino from compas_rhino.conversions import box_to_compas -from compas_rhino.conversions import xform_to_rhino +from compas_rhino.conversions import transformation_to_rhino from compas_rhino.conversions import sphere_to_rhino from compas_rhino.conversions import cylinder_to_rhino from compas_rhino.geometry.curves import RhinoCurve -import Rhino.Geometry +import Rhino.Geometry # type: ignore class RhinoSurface(Surface): @@ -25,13 +25,13 @@ class RhinoSurface(Surface): Attributes ---------- - u_domain: tuple[float, float] + domain_u: tuple[float, float] The parameter domain in the U direction. - v_domain: tuple[float, float] + domain_v: tuple[float, float] The parameter domain in the V direction. - is_u_periodic: bool + is_periodic_u: bool True if the surface is periodic in the U direction. - is_v_periodic: bool + is_periodic_v: bool True if the surface is periodic in the V direction. """ @@ -57,22 +57,22 @@ def rhino_surface(self, surface): # ============================================================================== @property - def u_domain(self): + def domain_u(self): if self.rhino_surface: return self.rhino_surface.Domain(0) @property - def v_domain(self): + def domain_v(self): if self.rhino_surface: return self.rhino_surface.Domain(1) @property - def is_u_periodic(self): + def is_periodic_u(self): if self.rhino_surface: return self.rhino_surface.IsPeriodic(0) @property - def is_v_periodic(self): + def is_periodic_v(self): if self.rhino_surface: return self.rhino_surface.IsPeriodic(1) @@ -235,7 +235,7 @@ def copy(self): """ cls = type(self) surface = cls() - surface.rhino_surface = self.rhino_surface.Duplicate() + surface.rhino_surface = self.rhino_surface.Duplicate() # type: ignore return surface def transform(self, T): @@ -251,7 +251,7 @@ def transform(self, T): None """ - self.rhino_surface.Transform(xform_to_rhino(T)) + self.rhino_surface.Transform(transformation_to_rhino(T)) # type: ignore def u_isocurve(self, u): """Compute the isoparametric curve at parameter u. @@ -265,7 +265,7 @@ def u_isocurve(self, u): :class:`~compas_rhino.geometry.RhinoCurve` """ - curve = self.rhino_surface.IsoCurve(1, u) + curve = self.rhino_surface.IsoCurve(1, u) # type: ignore return RhinoCurve.from_rhino(curve) def v_isocurve(self, v): @@ -280,7 +280,7 @@ def v_isocurve(self, v): :class:`~compas_rhino.geometry.RhinoCurve` """ - curve = self.rhino_surface.IsoCurve(0, v) + curve = self.rhino_surface.IsoCurve(0, v) # type: ignore return RhinoCurve.from_rhino(curve) def point_at(self, u, v): @@ -296,7 +296,7 @@ def point_at(self, u, v): :class:`~compas.geometry.Point` """ - point = self.rhino_surface.PointAt(u, v) + point = self.rhino_surface.PointAt(u, v) # type: ignore return point_to_compas(point) def curvature_at(self, u, v): @@ -315,7 +315,7 @@ def curvature_at(self, u, v): value for the point at UV. None at failure. """ - surface_curvature = self.rhino_surface.CurvatureAt(u, v) + surface_curvature = self.rhino_surface.CurvatureAt(u, v) # type: ignore if surface_curvature: point, normal, kappa_u, direction_u, kappa_v, direction_v, gaussian, mean = surface_curvature cpoint = point_to_compas(point) @@ -337,7 +337,7 @@ def frame_at(self, u, v): :class:`~compas.geometry.Frame` """ - result, plane = self.rhino_surface.FrameAt(u, v) + result, plane = self.rhino_surface.FrameAt(u, v) # type: ignore if result: return plane_to_compas_frame(plane) @@ -363,7 +363,7 @@ def closest_point(self, point, return_parameters=False): If `return_parameters` is True. """ - result, u, v = self.rhino_surface.ClosestPoint(point_to_rhino(point)) + result, u, v = self.rhino_surface.ClosestPoint(point_to_rhino(point)) # type: ignore if not result: return point = self.point_at(u, v) @@ -385,7 +385,7 @@ def aabb(self, precision=0.0, optimal=False): :class:`~compas.geometry.Box` """ - box = self.rhino_surface.GetBoundingBox(optimal) + box = self.rhino_surface.GetBoundingBox(optimal) # type: ignore return box_to_compas(Rhino.Geometry.Box(box)) def intersections_with_curve(self, curve, tolerance=1e-3, overlap=1e-3): diff --git a/src/compas_rhino/geometry/transformations/__init__.py b/src/compas_rhino/geometry/transformations/__init__.py deleted file mode 100644 index 47ae08eeacf..00000000000 --- a/src/compas_rhino/geometry/transformations/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import absolute_import - -import warnings - -warnings.warn( - "Conversion functions for transformations have been moved to `compas_rhino.conversions`.", - DeprecationWarning, -) - -from compas_rhino.conversions._transformations import * # noqa : F401 F403 diff --git a/tests/compas/artists/test_artists.py b/tests/compas/artists/test_artists.py index aca69ed6785..b6f693978bc 100644 --- a/tests/compas/artists/test_artists.py +++ b/tests/compas/artists/test_artists.py @@ -13,7 +13,7 @@ def reset_artists(): yield # after each test, reset artists Artist.ITEM_ARTIST.clear() - Artist._Artist__ARTISTS_REGISTERED = False + Artist._Artist__ARTISTS_REGISTERED = False # type: ignore def register_fake_context(): @@ -67,7 +67,7 @@ def test_get_artist_cls_with_out_of_order_registration(): def test_artist_auto_context_discovery(mocker): mocker.patch("compas.artists.Artist.register_artists") Artist.register_artists.side_effect = register_fake_context - Artist._Artist__ARTISTS_REGISTERED = False + Artist._Artist__ARTISTS_REGISTERED = False # type: ignore item = FakeItem() artist = Artist(item) @@ -83,15 +83,6 @@ def test_artist_auto_context_discovery_viewer(mocker): assert isinstance(artist, FakeArtist) - def test_artist_auto_context_discovery_plotter(mocker): - mocker.patch("compas.artists.artist.is_plotter_open", return_value=True) - Artist.ITEM_ARTIST["Plotter"] = {FakeItem: FakeArtist} - - item = FakeSubItem() - artist = Artist(item) - - assert isinstance(artist, FakeArtist) - def test_artist_auto_context_discovery_viewer_priority(mocker): mocker.patch("compas.artists.artist.is_viewer_open", return_value=True) @@ -109,26 +100,8 @@ class FakePlotterArtist(FakeArtist): assert isinstance(artist, FakeViewerArtist) - def test_artist_auto_context_discovery_manual_override(mocker): - mocker.patch("compas.artists.artist.is_viewer_open", return_value=True) - - class FakeViewerArtist(FakeArtist): - pass - - class FakePlotterArtist(FakeArtist): - pass - - Artist.ITEM_ARTIST["Viewer"] = {FakeItem: FakeViewerArtist} - Artist.ITEM_ARTIST["Plotter"] = {FakeItem: FakePlotterArtist} - - item = FakeSubItem() - artist = Artist(item, context="Plotter") - - assert isinstance(artist, FakePlotterArtist) - def test_artist_auto_context_discovery_no_context(mocker): mocker.patch("compas.artists.artist.is_viewer_open", return_value=False) - mocker.patch("compas.artists.artist.is_plotter_open", return_value=False) mocker.patch("compas.artists.artist.compas.is_grasshopper", return_value=False) mocker.patch("compas.artists.artist.compas.is_rhino", return_value=False)