diff --git a/CHANGELOG.md b/CHANGELOG.md index a036b42bad8..c8038a75b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Added `maxiter` parameter to `compas.geometry.icp_numpy`. - -### Changed - +* Added `resolution_u` and `resolution_v` to `compas.geometry.Shape` to control discretisation resolution. +* Added `vertices`, `edges`, `faces`, `triangles` to `compas.geometry.Shape`. +* Added `points`, `lines`, `polygons` to `compas.geometry.Shape`. +* Added abstract `compute_vertices`, `compute_edges`, `compute_faces`, `compute_triangles` to `compas.geometry.Shape`. +* Added implementation of `compute_vertices`, `compute_edges`, `compute_faces` to `compas.geometry.Box`. +* Added implementation of `compute_vertices`, `compute_edges`, `compute_faces` to `compas.geometry.Capsule`. +* Added implementation of `compute_vertices`, `compute_edges`, `compute_faces` to `compas.geometry.Cone`. +* Added implementation of `compute_vertices`, `compute_edges`, `compute_faces` to `compas.geometry.Cylinder`. +* Added implementation of `compute_vertices`, `compute_edges`, `compute_faces` to `compas.geometry.Sphere`. +* Added implementation of `compute_vertices`, `compute_edges`, `compute_faces` to `compas.geometry.Torus`. +* Added `compas_blender.scene.ShapeObject`. +* Added `compas.geometry.vector.__radd__`. +* Added `compas.geometry.vector.__rsub__`. +* Added `compas.geometry.vector.__rmul__`. +* Added `compas.geometry.vector.__rtruediv__`. +* Added `VolMesh.cell_lines`, `VolMesh.cell_polygons`. +* Added `VolMesh.vertex_edges`. +* Added `VolMesh.from_meshes`. +* Added `VolMesh.from_polyhedrons`. + +### Changed + +* Changed `compas_ghpython/utilities/drawing.py` to remove `System` dependency. * Fixed bug in `compas.geometry.ic_numpy`, which was caused by returning only the last transformation of the iteration process. * Changed `compas.geometry.Geometry.scaled` to use `compas.geometry.Geometry.scale` on a copy. * Changed `compas.geometry.Geometry.translated` to use `compas.geometry.Geometry.translate` on a copy. * Changed `compas.geometry.Geometry.rotated` to use `compas.geometry.Geometry.rotate` on a copy. - -### Removed - +* Changed `VolMesh._plane` back to point to a cell for every triplet of vertices. +* Fixed `VolMesh.add_halfface`, `VolMesh.add_cell`, `VolMesh.vertex_halffaces`, `VolMesh.vertex_cells`, `VolMesh.edge_halffaces`, `VolMesh.halfface_cell`, `VolMesh.halfface_opposite_cell`, `VolMesh.halfface_opposite_halfface`, `VolMesh.cell_neighbors`. +* Changed ordering of `Volmesh.edges()` to be deterministic. +* Changed ordering and direction of `Volmesh.vertex_edges()` to be deterministic. +* Changed check for empty vertices and faces to use `is None` to add support for `numpy` arrays. +* Changed order of `u` and `v` of `compas.geometry.SphericalSurface` to the match the excpected parametrisation. +* Changed `compas.geometry.Shape.to_vertices_and_faces` to use `Shape.vertices` and `Shape.faces` or `Shape.triangles`. +* Changed default of `compas.scene.descriptors.color.ColorAttribute` to `None` to support native coloring in CAD contexts. +* Changed `compas.colors.ColorDict.__data__` and `compas.colors.ColorDict.__from_data__` to properly support serialisation. +* Moved `compas_blender.utilities.drawing` to `compas_blender.drawing` with backward compatible imports and deprecation warning. +* Moved `compas_ghpython.utilities.drawing` to `compas_ghpython.drawing` with backward compatible imports and deprecation warning. +* Moved `compas_rhino.utilities.drawing` to `compas_rhino.drawing` with backward compatible imports and deprecation warning. +* Changed `draw_nodes` and `draw_edges` of `compas_blender.scene.GraphObject`, `compas_ghpython.scene.GraphObject`, and `compas_rhino.scene.GraphObject` to use only attributes instead of parameters. +* Changed `draw_vertices`, `draw_edges` and `draw_faces` of `compas_blender.scene.MeshObject`, `compas_ghpython.scene.MeshObject`, and `compas_rhino.scene.MeshObject` to use only attributes instead of parameters. +* Changed `draw_vertices`, `draw_edges` and `draw_faces` of `compas_blender.scene.VolMeshObject`, `compas_ghpython.scene.VolMeshObject`, and `compas_rhino.scene.VolMeshObject` to use only attributes instead of parameters. +* Changed registration of `Capsule`, `Cone`, `Cylinder`, `Sphere`, `Torus` to `ShapeObject` in `compas_blender.scene`. +* Updated `compas.geometry.vector.__mul__` to allow element-wise multiplication with another vector. +* Updated `compas.geometry.vector.__truediv__` to allow element-wise division with another vector. +* Fixed bug in registration `shapely` boolean plugins. +* Temporarily restrict `numpy` to versions lower than `2.x`. + +### Removed + +* Removed `System` dependency in `compas_ghpython/utilities/drawing.py`. +* Removed GH plugin for `compas.scene.clear` since it clashed with the Rhino version. ## [2.1.1] 2024-05-14 @@ -33,6 +75,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.tolerance.Tolerance.angulardeflection`. * Added `compas.tolerance.Tolerance.update_from_dict`. * Added `compas.scene.SceneObject.scene` attribute. +* Added `compas.datastructures.CellNetwork.is_faces_closed` +* Added `compas.datastructures.CellNetwork.delete_edge` +* Added `compas.datastructures.CellNetwork.delete_cell` +* Added `compas.datastructures.CellNetwork.delete_face` +* Added `compas.datastructures.CellNetwork.cells_to_graph` +* Added `compas.datastructures.CellNetwork.face_plane` +* Added `compas.datastructures.CellNetwork.cell_volume` +* Added `compas.datastructures.CellNetwork.cell_neighbors` ### Changed @@ -55,6 +105,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed `compas.tolerance.Tolerance` to a singleton, to ensure having only library-wide tolerance values. * Updated `compas_rhino.conversions.point_to_compas` to allow for `Rhino.Geometry.Point` as input. * Changed `compas.datastructures.Tree.print_hierarchy` to `compas.datastructures.Tree.__str__`. +* Changed `compas.scene.SceneObject.__init__` to accept `item` as kwarg. * Fixed `compas.geometry.bbox_numpy.minimum_volume_box` to avoid `numpy.linalg.LinAlgError`. ### Removed diff --git a/docs/_images/userguide/basics.datastructures.cellnetworks.example_color.png b/docs/_images/userguide/basics.datastructures.cellnetworks.example_color.png new file mode 100644 index 00000000000..4d2d0886973 Binary files /dev/null and b/docs/_images/userguide/basics.datastructures.cellnetworks.example_color.png differ diff --git a/docs/_images/userguide/basics.datastructures.cellnetworks.example_grey.png b/docs/_images/userguide/basics.datastructures.cellnetworks.example_grey.png new file mode 100644 index 00000000000..8f2a740579f Binary files /dev/null and b/docs/_images/userguide/basics.datastructures.cellnetworks.example_grey.png differ diff --git a/docs/_images/userguide/basics.datastructures.cellnetworks.example_hull.png b/docs/_images/userguide/basics.datastructures.cellnetworks.example_hull.png new file mode 100644 index 00000000000..29f355ecc98 Binary files /dev/null and b/docs/_images/userguide/basics.datastructures.cellnetworks.example_hull.png differ diff --git a/docs/api/compas.itertools.rst b/docs/api/compas.itertools.rst new file mode 100644 index 00000000000..fe28978e66e --- /dev/null +++ b/docs/api/compas.itertools.rst @@ -0,0 +1,27 @@ +******************************************************************************** +compas.itertools +******************************************************************************** + +.. currentmodule:: compas.itertools + +.. rst-class:: lead + +This package defines useful functions for working with iterable objects. + + +Functions +========= + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + normalize_values + remap_values + meshgrid + linspace + flatten + reshape + pairwise + window + iterable_like diff --git a/docs/api/index.rst b/docs/api/index.rst index 2ff94dbc664..ea028c7fa64 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -21,9 +21,11 @@ and can be used independently of CAD systems in any environment that supports Py compas.datastructures compas.files compas.geometry + compas.itertools compas.plugins compas.rpc compas.scene + compas.tolerance compas_blender diff --git a/docs/userguide/basics.datastructures.assemblies.rst b/docs/userguide/basics.datastructures.assemblies.rst deleted file mode 100644 index 7a15300a45b..00000000000 --- a/docs/userguide/basics.datastructures.assemblies.rst +++ /dev/null @@ -1,11 +0,0 @@ -******************************************************************************** -Assemblies -******************************************************************************** - -.. note:: - - The new (wip) version of the assembly data structure or model is available here - https://github.com/BRG-research/compas_assembly2. - - -More information coming soon... diff --git a/docs/userguide/basics.datastructures.cellnetwork.rst b/docs/userguide/basics.datastructures.cellnetwork.rst new file mode 100644 index 00000000000..fa6928bfc39 --- /dev/null +++ b/docs/userguide/basics.datastructures.cellnetwork.rst @@ -0,0 +1,137 @@ +******************************************************************************** +Cell Networks +******************************************************************************** + +.. rst-class:: lead + +A :class:`compas.datastructures.CellNetwork` uses a halfface data structure to represent a collection of mixed topologic entities (cells, faces, edges and vertices) +and to facilitate the application of topological and geometrical operations on it. +In addition, it provides a number of methods for storing arbitrary data on vertices, edges, faces, cells, and the overall cell network itself. + +.. note:: + + Please refer to the API for a complete overview of all functionality: + + * :class:`compas.datastructures.CellNetwork` + * :class:`compas.datastructures.HalfFace` + + +CellNetwork Construction +======================== + +CellNetworks can be constructed in a number of ways: + +* from scratch, by adding vertices, faces and cells one by one, +* using a special constructor function, or +* from the data contained in a file. + +From Scratch +------------ + +>>> from compas.datastructures import CellNetwork +>>> cell_network = CellNetwork() +>>> vertices = [(0, 0, 0), (0, 1, 0), (1, 1, 0), (1, 0, 0), (0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)] +>>> faces = [[0, 1, 2, 3], [0, 3, 5, 4], [3, 2, 6, 5], [2, 1, 7, 6], [1, 0, 4, 7], [4, 5, 6, 7]] +>>> cells = [[0, 1, 2, 3, 4, 5]] +>>> vertices = [cell_network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] +>>> faces = [cell_network.add_face(fverts) for fverts in faces] +>>> cells = [cell_network.add_cell(fkeys) for fkeys in cells] +>>> print(cell_network) + + +Using Constructors +------------------ + +>>> from compas.datastructures import CellNetwork +>>> cell_network = CellNetwork.from_vertices_and_cells(...) + + +From Data in a File +------------------- + +>>> from compas.datastructures import CellNetwork +>>> cell_network = CellNetwork.from_obj(...) +>>> cell_network = CellNetwork.from_json(...) + + +Visualisation +============= + +Like all other COMPAS geometry objects and data structures, cell networks can be visualised by placing them in a scene. +For more information about visualisation with :class:`compas.scene.Scene`, see :doc:`/userguide/basics.visualisation`. + +>>> import compas +>>> from compas.datastructures import CellNetwork +>>> from compas.scene import Scene +>>> cell_network = CellNetwork.from_json(compas.get('cellnetwork_example.json')) +>>> scene = Scene() +>>> scene.add(mesh) +>>> scene.show() + +.. figure:: /_images/userguide/basics.datastructures.cellnetworks.example_grey.png + + +Vertices, Edges, Faces, Cells +============================= + +The cell network contains mixed topologic entities such as cells, faces, edges and vertices. + * Vertices are identified by a positive integer that is unique among the vertices of the current mesh. + * Edges are identified by a pair (tuple) of two vertex identifiers. + * Faces are identified by a positive integer that is unique among the faces of the current mesh. + * Cells are identified by a positive integer that is unique among the cells of the current mesh. + +>>> cell_network = CellNetwork.from_json(compas.get('cellnetwork_example.json')) +>>> cell_network.number_of_vertices() +44 +>>> cell_network.number_of_edges() +91 +>>> cell_network.number_of_faces() +43 +>>> cell_network.number_of_cells() +6 + +An edge can be assigned to any number of faces, or to none. If an edge is assigned to more than 2 faces, it is non-manifold. + +>>> cell_network.edge_faces((2, 6)) +[2, 3, 39] +>>> cell_network.edge_faces((1, 10)) +[8] +>>> cell_network.edge_faces((43, 34)) +[] +>>> cell_network.edges_without_face() +[(43, 34)] +>>> nme = cell_network.nonmanifold_edges() + +A face can be at maximum assigned to two cells, to one or None. A face is on the boundary if is is exactly assigned to one cell. + +>>> cell_network.face_cells(7) +[12, 8] +>>> cell_network.face_cells(9) +[8] +>>> cell_network.faces_without_cell() +[34, 35, 36, 37, 38, 39] +>>> boundary = cell_network.faces_on_boundaries() +>>> boundary +[1, 2, 3, 5, 9, 10, 11, 13, 16, 17, 18, 20, 21, 22, 23, 24, 25, 26, 27, 30, 31, 32, 40, 41, 42, 43, 44, 49] + +If all cells are connected, those faces form a closed cell as well: + +>>> cell_network.is_faces_closed(boundary) +True + +This shows only the faces on the boundary displayed. + +.. figure:: /_images/userguide/basics.datastructures.cellnetworks.example_hull.png + + +If we want to add a cell, we need to provide a list of face keys that form a closed volume. +If they don't, the cell will not be added. + +In the following image, the faces belonging to 2 cells are showin in yellow, the faces to one cell are shown in grey, and the faces belonging to no cell are shown in blue. +There is also one edge without face, shown with thicker linewidth. + +.. figure:: /_images/userguide/basics.datastructures.cellnetworks.example_color.png + + + + diff --git a/docs/userguide/basics.datastructures.cellnetworks.py b/docs/userguide/basics.datastructures.cellnetworks.py new file mode 100644 index 00000000000..91206d53b18 --- /dev/null +++ b/docs/userguide/basics.datastructures.cellnetworks.py @@ -0,0 +1,34 @@ +import os + +from compas_viewer import Viewer + +import compas +from compas.colors import Color +from compas.datastructures import CellNetwork +from compas.geometry import Line +from compas.geometry import Polygon + +cell_network: CellNetwork = CellNetwork.from_json(compas.get("cellnetwork_example.json")) # type: ignore + +viewer = Viewer(show_grid=False) + +no_cell = cell_network.faces_without_cell() + +for face in cell_network.faces(): + if cell_network.is_face_on_boundary(face) is True: + color, opacity = Color.silver(), 0.5 + elif face in no_cell: + color, opacity = Color.azure(), 0.3 + else: + color, opacity = Color.yellow(), 0.8 + + viewer.scene.add(Polygon(cell_network.face_coordinates(face)), facecolor=color, opacity=opacity) + +for edge in cell_network.edges_without_face(): + line = Line(*cell_network.edge_coordinates(edge)) + viewer.scene.add(line, linewidth=3) + +graph = cell_network.cells_to_graph() +viewer.scene.add(graph) + +viewer.show() diff --git a/docs/userguide/basics.datastructures.meshes.rst b/docs/userguide/basics.datastructures.meshes.rst index 552eae77aae..096ab1ddf08 100644 --- a/docs/userguide/basics.datastructures.meshes.rst +++ b/docs/userguide/basics.datastructures.meshes.rst @@ -207,7 +207,7 @@ Halfedge Data Structure The topology of a mesh is stored in a halfedge data structure. In this data structure, vertices are connected to other vertices, and faces to other faces, via edges. An edge has two connected vertices, and at most two connected faces. -Each each is split into two halfedges, one for each of the connected faces. +Each edge is split into two halfedges, one for each of the connected faces. If an edge has only one connected face, the edge is on the boundary. Note that in a mesh constructed using :meth:`compas.datastructures.Mesh.from_meshgrid`, the vertices are organised in a specific way. diff --git a/docs/userguide/basics.datastructures.rst b/docs/userguide/basics.datastructures.rst index dddfd52f3eb..718780e5fe1 100644 --- a/docs/userguide/basics.datastructures.rst +++ b/docs/userguide/basics.datastructures.rst @@ -10,5 +10,5 @@ Datastructures basics.datastructures.graphs basics.datastructures.meshes basics.datastructures.cells + basics.datastructures.cellnetwork basics.datastructures.trees - basics.datastructures.assemblies diff --git a/docs/userguide/basics.datastructures.volmeshes.rst b/docs/userguide/basics.datastructures.volmeshes.rst new file mode 100644 index 00000000000..5d25cdd0243 --- /dev/null +++ b/docs/userguide/basics.datastructures.volmeshes.rst @@ -0,0 +1,367 @@ +******************************************************************************** +Vol(umetric) Meshes +******************************************************************************** + +.. currentmodule:: compas.datastructures + +.. rst-class:: lead + +A :class:`compas.datastructures.VolMesh` uses a "halfface" data structure to represent the topology and geometry of a cellular mesh, +and to facilitate the application of topological and geometrical operations on it. +In addition, it provides a number of methods for storing arbitrary data on vertices, edges, faces, and cell, and on the overall mesh itself. + +.. note:: + + Please refer to the API for a complete overview of all functionality: + + * :class:`compas.datastructures.VolMesh` + +General Properties +================== + +A `VolMesh` consists of vertices, edges, faces, and cells. +Currenly, a `VolMesh` can only be constructed by providing information about the vertices and cells. +This means that edges and faces are created implicitly, and can only exist as part of the topology of a cell. + +An edge connects exactly two vertices. +All edges of a valid `VolMesh` belong to at least one cell. + +A face consists of three or vertices, which are ordered such that they form a closed cycle. +The faces of a cell form a closed mesh. +The structure of this mesh is equivalent to the structure of a halfedge mesh (:class:`compas.datastructures.Mesh`). +The cycle directions of the faces are such that the resulting normals point towards the interior of the cell. + +Neighbouring cells share at least one face. +A face can be shared by at most two neighbouring cells. +The cycle directions of the faces through which two neighbouring cells are connected are exactly opposite. + +Naked faces are faces without an opposite face, or with an opposite face that doesn"t belong to a cell. +Cells with naked faces are considered to be on the boundary of the `VolMesh`. + +An empty `VolMesh` is valid. +A `VolMesh` with only vertices, but no cells is also valid. + +VolMesh Construction +==================== + +VolMeshes can be constructed in a number of ways: + +* from scratch, by adding vertices and cells one by one, +* from geometric input, +* using a special constructor function, or +* from the data contained in a file. + +.. note:: + + During construction, the validity of the input is not verified. + The `VolMesh` is created with the input that is provided. + Wrong input means wrong result. + +From Scratch +------------ + +The following snippet constructs a `VolMesh` with one cell in the form of a unit cube. +Note that the vertices of the faces are ordered such that the corresponding normal vectors point toward the interior of the cube. + +>>> from compas.datastructures import VolMesh +>>> volmesh = VolMesh() + +>>> a = volmesh.add_vertex(x=0, y=0, z=0) +>>> b = volmesh.add_vertex(x=1, y=0, z=0) +>>> c = volmesh.add_vertex(x=1, y=1, z=0) +>>> d = volmesh.add_vertex(x=0, y=1, z=0) +>>> e = volmesh.add_vertex(x=0, y=0, z=1) +>>> f = volmesh.add_vertex(x=1, y=0, z=1) +>>> g = volmesh.add_vertex(x=1, y=1, z=1) +>>> h = volmesh.add_vertex(x=0, y=1, z=1) + +>>> faces = [[a, b, c, d], [e, h, g, f], [a, e, f, b], [b, f, g, c], [c, g, h, d], [a, d, h, e]] +>>> cell = volmesh.add_cell(faces) + +Using Constructors +------------------ + +Constructing `VolMesh` objects "from scratch", as shown above, obviously quickly becomes rather tedious. +Therefore, the `VolMesh` class provides class methods that can be used to construct volumetric meshes +more easily from various types of common inputs. + +* :meth:`VolMesh.from_vertices_and_cells` +* :meth:`VolMesh.from_meshgrid` +* :meth:`VolMesh.from_meshes` +* :meth:`VolMesh.from_polyhedrons` + +>>> from compas.datastructures import VolMesh +>>> volmesh = VolMesh.from_obj("meshes.obj") + +From Data in a File +------------------- + +Currently only OBJ files containing closed meshes are supported. + +* :meth:`VolMesh.from_obj` + +>>> from compas.datastructures import VolMesh +>>> volmesh = VolMesh.from_obj("volmeshring.obj") + +Equivalently, the geometry of a `VolMesh` can also be written to an OBJ file. +However, note that all additional data attributes (see :ref:`Vertex, Edges, Face, Cell Attributes`) are lost during this process. +Only the geometry survives this conversion process. + +>>> volmesh.to_obj("volmeshring.obj") + +Visualisation +============= + +Like all other COMPAS geometry objects and data structures, volumetric meshes can be visualised by placing them in a scene. +For more information about visualisation with :class:`compas.scene.Scene`, see :doc:`/userguide/basics.visualisation`. +The snippet below uses `volmesh.obj`, which is available here: :download:`volmeshring.obj`. + +>>> from compas.datastructures import VolMesh +>>> from compas.scene import Scene +>>> mesh = Mesh.from_obj("volmeshring.obj") +>>> scene = Scene() +>>> scene.add(mesh) +>>> scene.show() + +.. figure:: /_images/userguide/basics.datastructures.volmeshes.volmeshring.png + +Vertices, Edges, Faces, Cells +============================= + +A `VolMesh` can only be constructed by providing information about the vertices and cells. +Edges are created implicitly, and can only exist as part of the topology of a cell. +Faces are not stored explicitly, but are generated automatically based on the corresponding (pairs of) halffaces. + +Vertices, faces, and cells are identified by unique, positive and increasing integers. +Edges are identified by pairs of vertex identifiers. + +In this section, we will use a `VolMesh` constructed from a mesh grid, +because its highly structured nature allows for easy and transparent counting and verification. + +>>> volmesh = VolMesh.from_meshgrid(dx=3, nx=3) + +.. figure:: /_images/userguide/basics.datastructures.volmeshes.volmeshgrid_3x3.png + +>>> volmesh.number_of_vertices() +64 +>>> volmesh.number_of_edges() +144 +>>> volmesh.number_of_faces() +108 +>>> volmesh.number_of_cells() +27 + +To iterate over vertices, edges, faces, and cells the `VolMesh` provides corresponding generators. + +>>> volmesh.vertices() + +>>> volmesh.edges() + +>>> volmesh.faces() + +>>> volmesh.cells() + + +These generators are meant to be used in loops. + +>>> for vertex in volmesh.vertices(): +... print(vertex) +... +0 +1 +2 +3 +4 +# etc. + +>>> for edge in volmesh.edges(): +... print(edge) +(0, 1) +(0, 4) +(0, 16) +(1, 2) +(1, 5) +# etc. + +The edges are not stored explicitly in the data structure, +but instead generated automatically from the adjacency information of the vertices. +They are generated following the order of the vertex identifiers to make sure the order of edges is deterministic. + +>>> for face in volmesh.faces(): +... print(face) +0 +1 +2 +3 +4 +# etc + +>>> for cell in volmesh.cells(): +... print(cell) +0 +1 +2 +3 +4 +# etc + +Lists of vertices, edges, faces, and cells have to be constructed explicitly. + +>>> vertices = list(volmesh.vertices()) +>>> vertices +[0, 1, 2, 3, 4, ..., 63] + +>>> edges = list(volmesh.edges()) +>>> edges +[(0, 1), (0, 4), (0, 16), (1, 2), (1, 5), ..., (62, 63)] + +# NOTE: this is not ideal. + +>>> faces = list(volmesh.faces()) +>>> faces +[???] + +>>> cells = list(volmesh.cells()) +>>> cells +[0, 1, 2, 3, 4, ..., 27] + +Vertex, Edge, Face, Cell Attributes +=================================== + +Arbitrary data can be assigned to vertices, edges, faces, and cells as vertex/edge/face/cell attributes, and to the overall `VolMesh` itself. +To allow for serialisatin of the `VolMesh` and all the data associated with it, the data should be JSON serialisable. +See :ref:`Data Serialisation` for more information. + +The functionality is demonstrated here using vertex attributes. +The mechanism is exactly the same for edges, faces, and cells. + +It is good practice to declare default values for the added data attributes. + +>>> volmesh = VolMesh.from_meshgrid(dx=3, nx=10) +>>> volmesh.update_default_vertex_attributes(a=None, b=0.0, c=False) + +Get the value of one attribute of one vertex. + +>>> volmesh.vertex_attribute(vertex=0, name="a") +None + +Get the value of multiple attributes of one vertex. + +>>> volmesh.vertex_attributes(vertex=0, names=["a", "b"]) +(None, 0.0) + +Get the value of one attribute of all vertices. + +>>> volmesh.vertices_attribute(name="a") +[None, None, None, ... None] + +Get the value of multiple attributes of all vertices. + +>>> volmesh.vertices_attributes(names=["b", "c"]) +[(0.0, False), (0.0, False), (0.0, False), ..., (0.0, False)] + +Similarly, for a selection of vertices. + +>>> volmesh.vertices_attribute(name="b", vertices=[0, 1, 2, 3]) +[0.0, 0.0, 0.0, 0.0] + +>>> volmesh.vertices_attributes(names=["a", "c"], vertices=[0, 1, 2, 3]) +[(None, False), (None, False), (None, False), (None, False)] + +Updating attributes is currently only possible one vertex at a time. + +>>> volmesh.vertex_attribute(vertex=0, name="a", value=(1.0, 0.0, 0.0)) + +>>> for vertex in volmesh.vertices(): +... if volmesh.vertex_degree(vertex) == 2: +... volmesh.vertex_attribute(vertex=vertex, name="a", value=(1.0, 0.0, 0.0)) +... + +Overall Topology +================ + +More info coming... + +* vertex_neighbors +* vertex_cells +* vertex_edges +* vertex_faces +* edge_cells +* edge_faces +* edge_halffaces +* cell_neighbors + +Topology of a Cell +================== + +Within the context of a cell, the topology of a `VolMesh` is the same as the topology of a regular `Mesh`. +Vertices are connected to other vertices, and faces to other faces, via edges. +An edge has two connected vertices, and at most two connected faces. +Each edge is split into two halfedges, one for each of the connected faces. + +>>> volmesh = VolMesh.from_meshgrid(dx=3, nx=3) +>>> cell = volmesh.cell_sample(size=1).next() +>>> + +* cell_vertices +* cell_edges +* cell_halfedges +* cell_halffaces +* cell_faces +* cell_vertex_halffaces +* cell_vertex_faces +* cell_vertex_neighbors +* cell_edge_halffaces +* cell_edge_faces + +.. edges and faces are implicit, and only used to store additional data +.. their identifiers are also implicit +.. halfedge_face +.. face_attributes +.. + +Geometry +======== + +* vertex_point +* edge_vector +* edge_line +* edge_start +* edge_end +* face_centroid +* face_area +* halfface_centroid +* halfface_polygon +* halfface_normal +* cell_volume +* cell_centroid +* cell_polyhedron + +Filtering +========= + +* vertices_where +* edges_where +* faces_where +* cells_where + +Data Serialisation +================== + +>>> volmesh.to_json("volmesh.json") +>>> volmesh = VolMesh.from_json("volmesh.json") + +>>> s = volmesh.to_jsonstring() +>>> volmesh = VolMesh.from_jsonstring(s) + +>>> session = {"volmesh": volmesh, "a": 1, "b": 2} +>>> compas.json_dump(session, "session.json") +>>> session = compas.json_load("session.json") +>>> volmesh = session["volmesh"] + +Examples +======== + +.. literalinclude:: basics.datastructures.volmeshes_example-1.py + :language: python + diff --git a/docs/userguide/basics.datastructures.volmeshes_example-1.py b/docs/userguide/basics.datastructures.volmeshes_example-1.py new file mode 100644 index 00000000000..2fcaba2419b --- /dev/null +++ b/docs/userguide/basics.datastructures.volmeshes_example-1.py @@ -0,0 +1,80 @@ +from math import radians + +import numpy +from compas_viewer import Viewer +from compas_viewer.config import Config +from compas_viewer.scene import BufferGeometry + +from compas.colors import Color +from compas.datastructures import VolMesh +from compas.geometry import Box +from compas.geometry import Plane +from compas.tolerance import Tolerance + +tolerance = Tolerance() + +# ============================================================================= +# Base Box +# ============================================================================= + +box = Box(10).to_mesh() + +# ============================================================================= +# Cutting Planes +# ============================================================================= + +planes = [ + Plane.worldXY().rotated(radians(30), [0, 1, 0]), + Plane.worldYZ().rotated(radians(30), [0, 0, 1]), + Plane.worldZX().rotated(radians(30), [1, 0, 0]), + Plane.worldXY().translated([0, 0, +2.5]), + Plane.worldXY().translated([0, 0, -2.5]), +] + +# ============================================================================= +# Cuts +# ============================================================================= + +results = [box] +for plane in planes: + temp = [] + for box in results: + result = box.slice(plane) + if result: + temp += result + else: + temp.append(box) + results = temp + +# ============================================================================= +# VolMesh Construction +# ============================================================================= + +volmesh = VolMesh.from_meshes(results) + +volmesh.translate([5, 5, 5]) + +# ============================================================================= +# Visualisation +# ============================================================================= + +config = Config() +config.camera.target = [5, 5, 5] +config.camera.position = [-8, -15, 10] + +viewer = Viewer(config=config) + +wires = numpy.asarray([volmesh.edge_coordinates(edge) for edge in volmesh.edges()]) +viewer.scene.add(BufferGeometry(lines=wires), name="Wires") + +cell = list(volmesh.cell_sample(size=1))[0] +cell_color = {cell: Color.red()} +for nbr in volmesh.cell_neighbors(cell): + cell_color[nbr] = Color.green() + +for cell in cell_color: + color = cell_color[cell] + mesh = volmesh.cell_to_mesh(cell) + viewer.scene.add(mesh, facecolor=color) + +viewer.show() diff --git a/requirements.txt b/requirements.txt index 11f9a773638..77175e1c10d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # flake8: noqa jsonschema networkx >= 3.0 -numpy >= 1.15.4 +numpy >= 1.15.4, < 2 scipy >= 1.1 watchdog; sys_platform != 'emscripten' diff --git a/src/compas/__init__.py b/src/compas/__init__.py index f42a41991ed..3a11ce36201 100644 --- a/src/compas/__init__.py +++ b/src/compas/__init__.py @@ -131,7 +131,7 @@ ] __all_plugins__ = [ - "compas.geometry.booleans.booleans_shapely", + "compas.geometry.booleans_shapely", "compas.scene", ] diff --git a/src/compas/colors/colordict.py b/src/compas/colors/colordict.py index 5a86b8fa46a..916da688de7 100644 --- a/src/compas/colors/colordict.py +++ b/src/compas/colors/colordict.py @@ -26,7 +26,15 @@ class ColorDict(Data): @property def __data__(self): - return {"default": self.default.__data__, "dict": self._dict} + return {"default": self.default, "dict": self._dict} + + @classmethod + def __from_data__(cls, data): + colordict = cls(data["default"]) + # note: this is specific to color dicts for vertices of meshes + # perhaps the color dict needs to be subclassed per scene object type + colordict.update({int(key): value for key, value in data["dict"].items()}) + return colordict def __init__(self, default, name=None): super(ColorDict, self).__init__(name=name) diff --git a/src/compas/data/samples/cellnetwork_example.json b/src/compas/data/samples/cellnetwork_example.json new file mode 100644 index 00000000000..6d7eb4e7ef5 --- /dev/null +++ b/src/compas/data/samples/cellnetwork_example.json @@ -0,0 +1,744 @@ +{ + "dtype": "compas.datastructures/CellNetwork", + "data": { + "attributes": {}, + "default_vertex_attributes": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "default_edge_attributes": {}, + "default_face_attributes": {}, + "default_cell_attributes": {}, + "vertex": { + "0": { + "x": 8.0, + "y": -1.5, + "z": 3.0 + }, + "1": { + "x": 8.0, + "y": 8.0, + "z": 3.0 + }, + "2": { + "x": 12.5, + "y": 8.0, + "z": 3.0 + }, + "3": { + "x": 12.5, + "y": -1.5, + "z": 3.0 + }, + "4": { + "x": 8.0, + "y": -1.5, + "z": 6.0 + }, + "5": { + "x": 12.5, + "y": -1.5, + "z": 6.0 + }, + "6": { + "x": 12.5, + "y": 8.0, + "z": 6.0 + }, + "7": { + "x": 8.0, + "y": 8.0, + "z": 6.0 + }, + "8": { + "x": 5.0, + "y": 3.0, + "z": 3.0 + }, + "9": { + "x": 5.0, + "y": 8.0, + "z": 3.0 + }, + "10": { + "x": 8.0, + "y": 3.0, + "z": 3.0 + }, + "11": { + "x": 5.0, + "y": 3.0, + "z": 6.0 + }, + "12": { + "x": 8.0, + "y": 3.0, + "z": 6.0 + }, + "13": { + "x": 5.0, + "y": 8.0, + "z": 6.0 + }, + "14": { + "x": 0.0, + "y": -1.5, + "z": 3.0 + }, + "15": { + "x": 0.0, + "y": 3.0, + "z": 3.0 + }, + "16": { + "x": 0.0, + "y": -1.5, + "z": 6.0 + }, + "17": { + "x": 0.0, + "y": 3.0, + "z": 6.0 + }, + "18": { + "x": 0.0, + "y": 6.5, + "z": 0.0 + }, + "19": { + "x": 0.0, + "y": 11.5, + "z": 0.0 + }, + "20": { + "x": 5.0, + "y": 11.5, + "z": 0.0 + }, + "21": { + "x": 5.0, + "y": 6.5, + "z": 0.0 + }, + "22": { + "x": 0.0, + "y": 6.5, + "z": 3.0 + }, + "23": { + "x": 5.0, + "y": 6.5, + "z": 3.0 + }, + "24": { + "x": 5.0, + "y": 11.5, + "z": 3.0 + }, + "25": { + "x": 0.0, + "y": 11.5, + "z": 3.0 + }, + "26": { + "x": 5.0, + "y": 0.0, + "z": 0.0 + }, + "27": { + "x": 12.5, + "y": 6.5, + "z": 0.0 + }, + "28": { + "x": 12.5, + "y": 0.0, + "z": 0.0 + }, + "29": { + "x": 5.0, + "y": 0.0, + "z": 3.0 + }, + "30": { + "x": 12.5, + "y": 0.0, + "z": 3.0 + }, + "31": { + "x": 12.5, + "y": 6.5, + "z": 3.0 + }, + "32": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "33": { + "x": 0.0, + "y": 0.0, + "z": 3.0 + }, + "34": { + "x": 0.0, + "y": 8.0, + "z": 6.0 + }, + "35": { + "x": 14.5, + "y": -1.5, + "z": 6.0 + }, + "36": { + "x": 14.5, + "y": -1.5, + "z": 3.0 + }, + "37": { + "x": 14.5, + "y": 8.0, + "z": 6.0 + }, + "38": { + "x": 14.5, + "y": 8.0, + "z": 3.0 + }, + "39": { + "x": 14.5, + "y": 8.0, + "z": 4.2 + }, + "40": { + "x": 14.5, + "y": -1.5, + "z": 4.2 + }, + "41": { + "x": 8.0, + "y": 0.0, + "z": 3.0 + }, + "42": { + "x": 8.0, + "y": 6.5, + "z": 3.0 + }, + "43": { + "x": 0, + "y": 8, + "z": 3 + } + }, + "edge": { + "0": { + "3": {}, + "4": {}, + "10": {}, + "41": {} + }, + "1": { + "9": {} + }, + "2": { + "1": {}, + "31": {} + }, + "3": { + "2": {}, + "36": {} + }, + "4": { + "5": {}, + "12": {} + }, + "5": { + "3": {}, + "6": {} + }, + "6": { + "2": {}, + "7": {} + }, + "7": { + "1": {}, + "4": {}, + "13": {} + }, + "8": { + "10": {}, + "11": {}, + "15": {} + }, + "9": { + "8": {} + }, + "10": { + "1": {}, + "41": {}, + "42": {} + }, + "11": { + "12": {}, + "17": {} + }, + "12": { + "10": {}, + "7": {}, + "17": {} + }, + "13": { + "9": {}, + "11": {} + }, + "14": { + "0": {}, + "16": {}, + "33": {} + }, + "15": { + "14": {}, + "22": {} + }, + "16": { + "4": {} + }, + "17": { + "15": {}, + "16": {}, + "34": {} + }, + "18": { + "21": {}, + "22": {}, + "32": {} + }, + "19": { + "18": {} + }, + "20": { + "19": {} + }, + "21": { + "20": {}, + "26": {} + }, + "22": { + "23": {}, + "33": {} + }, + "23": { + "21": {}, + "24": {}, + "29": {}, + "8": {}, + "9": {} + }, + "24": { + "20": {}, + "25": {} + }, + "25": { + "19": {}, + "22": {} + }, + "26": { + "28": {}, + "29": {} + }, + "27": { + "21": {} + }, + "28": { + "27": {} + }, + "29": { + "30": {}, + "41": {}, + "8": {} + }, + "30": { + "28": {}, + "31": {}, + "3": {} + }, + "31": { + "27": {}, + "23": {}, + "42": {} + }, + "32": { + "26": {}, + "33": {} + }, + "33": { + "29": {}, + "15": {} + }, + "34": { + "13": {}, + "43": {} + }, + "35": { + "5": {}, + "37": {} + }, + "36": { + "35": {}, + "38": {} + }, + "37": { + "6": {}, + "38": {} + }, + "38": { + "2": {}, + "39": {} + }, + "39": { + "40": {} + }, + "40": { + "36": {} + }, + "41": { + "30": {} + }, + "42": { + "1": {}, + "23": {} + }, + "43": {} + }, + "face": { + "1": [ + 0, + 3, + 5, + 4 + ], + "2": [ + 3, + 2, + 6, + 5 + ], + "3": [ + 2, + 1, + 7, + 6 + ], + "5": [ + 4, + 5, + 6, + 7 + ], + "7": [ + 8, + 10, + 12, + 11 + ], + "8": [ + 10, + 1, + 7, + 12 + ], + "9": [ + 1, + 9, + 13, + 7 + ], + "10": [ + 9, + 8, + 11, + 13 + ], + "11": [ + 11, + 12, + 7, + 13 + ], + "13": [ + 14, + 0, + 4, + 16 + ], + "14": [ + 0, + 10, + 12, + 4 + ], + "16": [ + 15, + 14, + 16, + 17 + ], + "17": [ + 16, + 4, + 12, + 17 + ], + "18": [ + 18, + 19, + 20, + 21 + ], + "19": [ + 18, + 21, + 23, + 22 + ], + "20": [ + 21, + 20, + 24, + 23 + ], + "21": [ + 20, + 19, + 25, + 24 + ], + "22": [ + 19, + 18, + 22, + 25 + ], + "23": [ + 22, + 23, + 24, + 25 + ], + "24": [ + 26, + 21, + 27, + 28 + ], + "25": [ + 26, + 28, + 30, + 29 + ], + "26": [ + 28, + 27, + 31, + 30 + ], + "27": [ + 27, + 21, + 23, + 31 + ], + "28": [ + 21, + 26, + 29, + 23 + ], + "30": [ + 32, + 18, + 21, + 26 + ], + "31": [ + 32, + 26, + 29, + 33 + ], + "32": [ + 18, + 32, + 33, + 22 + ], + "34": [ + 13, + 34, + 17, + 11 + ], + "35": [ + 35, + 36, + 3, + 5 + ], + "36": [ + 5, + 6, + 37, + 35 + ], + "37": [ + 3, + 2, + 38, + 36 + ], + "38": [ + 39, + 38, + 36, + 40 + ], + "39": [ + 6, + 2, + 38, + 37 + ], + "40": [ + 0, + 3, + 30, + 41 + ], + "41": [ + 2, + 1, + 42, + 31 + ], + "42": [ + 23, + 22, + 15, + 8 + ], + "43": [ + 1, + 9, + 23, + 42 + ], + "44": [ + 0, + 41, + 29, + 33, + 14 + ], + "45": [ + 10, + 8, + 29, + 41 + ], + "46": [ + 41, + 30, + 31, + 42, + 10 + ], + "47": [ + 29, + 8, + 15, + 33 + ], + "48": [ + 8, + 10, + 42, + 23 + ], + "49": [ + 17, + 15, + 8, + 11 + ] + }, + "cell": { + "3": [ + 18, + 19, + 20, + 21, + 22, + 23 + ], + "7": [ + 19, + 28, + 30, + 31, + 32, + 42, + 47 + ], + "8": [ + 7, + 8, + 9, + 10, + 11, + 43, + 48 + ], + "10": [ + 24, + 25, + 26, + 27, + 28, + 45, + 46, + 48 + ], + "11": [ + 1, + 2, + 3, + 5, + 8, + 14, + 40, + 41, + 46 + ], + "12": [ + 7, + 13, + 14, + 16, + 17, + 44, + 45, + 47, + 49 + ] + }, + "face_data": {}, + "cell_data": {}, + "max_vertex": 43, + "max_face": 49, + "max_cell": 12 + }, + "guid": "d9706146-662b-455e-958f-9bc415dc92a2" +} diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 59f7332f76d..6e49e289e6d 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -1,7 +1,9 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division from __future__ import print_function +from ast import literal_eval from random import sample from compas.datastructures import Graph @@ -13,6 +15,7 @@ from compas.datastructures.datastructure import Datastructure from compas.files import OBJ from compas.geometry import Line +from compas.geometry import Plane from compas.geometry import Point from compas.geometry import Polygon from compas.geometry import Polyhedron @@ -128,6 +131,11 @@ class CellNetwork(Datastructure): }, "additionalProperties": False, }, + "edge_data": { + "type": "object", + "patternProperties": {"^\\([0-9]+(, [0-9]+){3, }\\)$": {"type": "object"}}, + "additionalProperties": False, + }, "face_data": { "type": "object", "patternProperties": {"^\\([0-9]+(, [0-9]+){3, }\\)$": {"type": "object"}}, @@ -152,6 +160,7 @@ class CellNetwork(Datastructure): "edge", "face", "cell", + "edge_data", "face_data", "cell_data", "max_vertex", @@ -180,6 +189,7 @@ def __data__(self): "edge": self._edge, "face": self._face, "cell": cell, + "edge_data": {str(k): v for k, v in self._edge_data.items()}, "face_data": self._face_data, "cell_data": self._cell_data, "max_vertex": self._max_vertex, @@ -203,19 +213,21 @@ def __from_data__(cls, data): cell = data["cell"] or {} for key, attr in iter(vertex.items()): - cell_network.add_vertex(key=key, attr_dict=attr) + cell_network.add_vertex(key=int(key), attr_dict=attr) + edge_data = {literal_eval(k): v for k, v in data.get("edge_data", {}).items()} for u in edge: - for v, attr in edge[u].items(): - cell_network.add_edge(u, v, attr_dict=attr) + for v in edge[u]: + attr = edge_data.get(tuple(sorted((int(u), int(v)))), {}) + cell_network.add_edge(int(u), int(v), attr_dict=attr) face_data = data.get("face_data") or {} for key, vertices in iter(face.items()): - cell_network.add_face(vertices, fkey=key, attr_dict=face_data.get(key)) + cell_network.add_face(vertices, fkey=int(key), attr_dict=face_data.get(key)) cell_data = data.get("cell_data") or {} for ckey, faces in iter(cell.items()): - cell_network.add_cell(faces, ckey=ckey, attr_dict=cell_data.get(ckey)) + cell_network.add_cell(faces, ckey=int(ckey), attr_dict=cell_data.get(ckey)) cell_network._max_vertex = data.get("max_vertex", cell_network._max_vertex) cell_network._max_face = data.get("max_face", cell_network._max_face) @@ -233,6 +245,7 @@ def __init__(self, default_vertex_attributes=None, default_edge_attributes=None, self._face = {} self._plane = {} self._cell = {} + self._edge_data = {} self._face_data = {} self._cell_data = {} self.default_vertex_attributes = {"x": 0.0, "y": 0.0, "z": 0.0} @@ -526,20 +539,24 @@ def add_edge(self, u, v, attr_dict=None, **kwattr): attr = attr_dict or {} attr.update(kwattr) - data = self._edge[u].get(v, {}) + uv = tuple(sorted((u, v))) + + data = self._edge_data.get(uv, {}) data.update(attr) - self._edge[u][v] = data + self._edge_data[uv] = data # @Romana # should the data not be added to this edge as well? # if that is the case, should we not store the data in an edge_data dict to avoid duplication? - # if v not in self._edge[u]: - # self._edge[v][u] = {} + # True, but then _edge does not hold anything, we could also store the attr right here. + # but I leave this to you as you have a better overview - # if v not in self._plane[u]: - # self._plane[u][v] = {} - # if u not in self._plane[v]: - # self._plane[v][u] = {} + if v not in self._edge[u]: + self._edge[v][u] = {} + if v not in self._plane[u]: + self._plane[u][v] = {} + if u not in self._plane[v]: + self._plane[v][u] = {} return u, v @@ -614,6 +631,27 @@ def add_face(self, vertices, fkey=None, attr_dict=None, **kwattr): return fkey + def _faces_to_unified_mesh(self, faces): + faces = list(set(faces)) + # 0. Check if all the faces have been added + for face in faces: + if face not in self._face: + raise ValueError("Face {} does not exist.".format(face)) + # 2. Check if the faces can be unified + mesh = self.faces_to_mesh(faces, data=False) + try: + mesh.unify_cycles() + except Exception: + return None + return mesh + + def is_faces_closed(self, faces): + """Checks if the faces form a closed cell.""" + mesh = self._faces_to_unified_mesh(faces) + if mesh: + return True + return False + def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): """Add a cell to the cell network object. @@ -638,6 +676,8 @@ def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): Raises ------ + ValueError + If something is wrong with the passed faces. TypeError If the provided cell key is not an integer. @@ -651,40 +691,14 @@ def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): highest integer key value, then the highest integer value is updated accordingly. """ - faces = list(set(faces)) - - # 0. Check if all the faces have been added - for face in faces: - if face not in self._face: - raise Exception("Face {} does not exist.".format(face)) - - # @Romana - # i think this is implicitly checked by the next step - - # # 1. Check if the faces form a closed cell - # # Note: We cannot use mesh.is_closed() here because it only works if the faces are unified - # if any(len(edge_faces) != 2 for _, edge_faces in self.edge_face_adjacency(faces).items()): - # print("Cannot add cell, faces {} do not form a closed cell.".format(faces)) - # return - - # 2. Check if the faces can be unified - mesh = self.faces_to_mesh(faces, data=False) - try: - mesh.unify_cycles() - except Exception: - # @Romana - # should we not throw an exception here? - print("Cannot add cell, faces {} can not be unified.".format(faces)) - return - - # @Romana: should it not be the other way around? - # a polyhedron has poisitve volume if the faces point outwards - # the faces of the cell however should point inwards + mesh = self._faces_to_unified_mesh(faces) + if mesh is None: + raise ValueError("Cannot add cell, faces {} do not form a closed cell.".format(faces)) # 3. Check if the faces are oriented correctly - # If the volume of the polyhedron is negative, we need to flip the faces + # If the volume of the polyhedron is positive, we need to flip the faces to point inwards volume = volume_polyhedron(mesh.to_vertices_and_faces()) - if volume < 0: + if volume > 0: mesh.flip_cycles() if ckey is None: @@ -734,49 +748,96 @@ def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): # for cell in self.vertex_cells(vertex): # self.delete_cell(cell) - # def delete_cell(self, cell): - # """Delete a cell from the cell network. + def delete_edge(self, edge): + """Delete an edge from the cell network. - # Parameters - # ---------- - # cell : int - # The identifier of the cell. + Parameters + ---------- + edge : tuple + The identifier of the edge. - # Returns - # ------- - # None + Returns + ------- + None - # See Also - # -------- - # :meth:`delete_vertex`, :meth:`delete_halfface` + """ + u, v = edge + if self._plane[u] and v in self._plane[u]: + faces = self._plane[u][v].keys() + if len(faces) > 0: + print("Cannot delete edge %s, delete faces %s first" % (edge, list(faces))) + return + if self._plane[v] and u in self._plane[v]: + faces = self._plane[v][u].keys() + if len(faces) > 0: + print("Cannot delete edge %s, delete faces %s first" % (edge, list(faces))) + return + if v in self._edge[u]: + del self._edge[u][v] + if u in self._edge[v]: + del self._edge[v][u] + if v in self._plane[u]: + del self._plane[u][v] + if u in self._plane[v]: + del self._plane[v][u] - # """ - # cell_vertices = self.cell_vertices(cell) - # cell_faces = self.cell_faces(cell) - # for face in cell_faces: - # for edge in self.halfface_halfedges(face): - # u, v = edge - # if (u, v) in self._edge_data: - # del self._edge_data[u, v] - # if (v, u) in self._edge_data: - # del self._edge_data[v, u] - # for vertex in cell_vertices: - # if len(self.vertex_cells(vertex)) == 1: - # del self._vertex[vertex] - # for face in cell_faces: - # vertices = self.halfface_vertices(face) - # for u, v in iter_edges_from_vertices(vertices): - # self._plane[u][v][face] = None - # if self._plane[v][u][face] is None: - # del self._plane[u][v][face] - # del self._plane[v][u][face] - # del self._halfface[face] - # key = "-".join(map(str, sorted(vertices))) - # if key in self._face_data: - # del self._face_data[key] - # del self._cell[cell] - # if cell in self._cell_data: - # del self._cell_data[cell] + def delete_face(self, face): + """Delete a face from the cell network. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + None + """ + vertices = self.face_vertices(face) + # check first + for u, v in pairwise(vertices + vertices[:1]): + if self._plane[u][v][face] is not None: + print("Cannot delete face %d, delete cell %s first" % (face, self._plane[u][v][face])) + return + if self._plane[v][u][face] is not None: + print("Cannot delete face %d, delete cell %s first" % (face, self._plane[v][u][face])) + return + for u, v in pairwise(vertices + vertices[:1]): + del self._plane[u][v][face] + del self._plane[v][u][face] + del self._face[face] + if face in self._face_data: + del self._face_data[face] + + def delete_cell(self, cell): + """Delete a cell from the cell network. + + Parameters + ---------- + cell : int + The identifier of the cell. + + Returns + ------- + None + + See Also + -------- + :meth:`delete_vertex`, :meth:`delete_halfface` + + """ + # remove the cell from the faces + cell_faces = self.cell_faces(cell) + for face in cell_faces: + vertices = self.face_vertices(face) + for u, v in pairwise(vertices + vertices[:1]): + if self._plane[u][v][face] == cell: + self._plane[u][v][face] = None + if self._plane[v][u][face] == cell: + self._plane[v][u][face] = None + del self._cell[cell] + if cell in self._cell_data: + del self._cell_data[cell] # def remove_unused_vertices(self): # """Remove all unused vertices from the cell network object. @@ -1012,6 +1073,24 @@ def edges_to_graph(self): graph.add_edge(u, v, attr_dict=attr) return graph + def cells_to_graph(self): + """Convert the cells the cell network to a graph. + + Returns + ------- + :class:`compas.datastructures.Graph` + A graph object. + + """ + graph = Graph() + for cell, attr in self.cells(data=True): + x, y, z = self.cell_centroid(cell) + graph.add_node(key=cell, x=x, y=y, z=z, attr_dict=attr) + for cell in self.cells(): + for nbr in self.cell_neighbors(cell): + graph.add_edge(*sorted([cell, nbr])) + return graph + def cell_to_vertices_and_faces(self, cell): """Return the vertices and faces of a cell. @@ -1868,12 +1947,13 @@ def edges(self, data=False): """ seen = set() for u, nbrs in iter(self._edge.items()): - for v, attr in iter(nbrs.items()): + for v in nbrs: if (u, v) in seen or (v, u) in seen: continue seen.add((u, v)) seen.add((v, u)) if data: + attr = self._edge_data[tuple(sorted([u, v]))] yield (u, v), attr else: yield u, v @@ -2030,10 +2110,11 @@ def edge_attribute(self, edge, name, value=None): if not self.has_edge(edge): raise KeyError(edge) - u, v = edge - attr = self._edge[u][v] + attr = self._edge_data.get(tuple(sorted(edge)), {}) + if value is not None: - attr[name] = value + attr.update({name: value}) + self._edge_data[tuple(sorted(edge))] = attr return if name in attr: return attr[name] @@ -2072,8 +2153,7 @@ def unset_edge_attribute(self, edge, name): if not self.has_edge(edge): raise KeyError(edge) - u, v = edge - del self._edge[u][v][name] + del self._edge_data[tuple(sorted(edge))][name] def edge_attributes(self, edge, names=None, values=None): """Get or set multiple attributes of an edge. @@ -2108,14 +2188,12 @@ def edge_attributes(self, edge, names=None, values=None): if not self.has_edge(edge): raise KeyError(edge) - u, v = edge - if names and values: for name, value in zip(names, values): - self._edge[u][v][name] = value + self._edge_data[tuple(sorted(edge))][name] = value return if not names: - return EdgeAttributeView(self.default_edge_attributes, self._edge[u][v]) + return EdgeAttributeView(self.default_edge_attributes, self._edge_data[tuple(sorted(edge))]) values = [] for name in names: value = self.edge_attribute(edge, name) @@ -2302,7 +2380,7 @@ def edge_cells(self, edge): # u, v = edge # return None in self._plane[u][v].values() - def isolated_edges(self): + def edges_without_face(self): """Find the edges that are not part of a face. Returns @@ -2951,7 +3029,7 @@ def face_cells(self, face): cells.append(cell) return cells - def isolated_faces(self): + def faces_without_cell(self): """Find the faces that are not part of a cell. Returns @@ -2964,6 +3042,7 @@ def isolated_faces(self): return list(faces) # @Romana: this logic only makes sense for a face belonging to a cell + # # yep, if the face is not belonging to a cell, it returns False, which is correct def is_face_on_boundary(self, face): """Verify that a face is on the boundary. @@ -3145,6 +3224,26 @@ def face_area(self, face): """ return length_vector(self.face_normal(face, unitized=False)) + def face_plane(self, face): + """Compute the plane of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + :class:`compas.geometry.Plane` + The plane of the face. + + See Also + -------- + :meth:`face_points`, :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_centroid`, :meth:`face_center` + + """ + return Plane(self.face_centroid(face), self.face_normal(face)) + def face_flatness(self, face, maxdev=0.02): """Compute the flatness of a face. @@ -3929,30 +4028,29 @@ def cell_face_neighbors(self, cell, face): nbrs.append(nbr) return nbrs - # def cell_neighbors(self, cell): - # """Find the neighbors of a given cell. - - # Parameters - # ---------- - # cell : int - # The identifier of the cell. + def cell_neighbors(self, cell): + """Find the neighbors of a given cell. - # Returns - # ------- - # list[int] - # The identifiers of the adjacent cells. + Parameters + ---------- + cell : int + The identifier of the cell. - # See Also - # -------- - # :meth:`cell_face_neighbors` + Returns + ------- + list[int] + The identifiers of the adjacent cells. - # """ - # nbrs = [] - # for face in self.cell_faces(cell): - # nbr = self.halfface_opposite_cell(face) - # if nbr is not None: - # nbrs.append(nbr) - # return nbrs + See Also + -------- + :meth:`cell_face_neighbors` + """ + nbrs = [] + for face in self.cell_faces(cell): + for nbr in self.face_cells(face): + if nbr != cell: + nbrs.append(nbr) + return list(set(nbrs)) def is_cell_on_boundary(self, cell): """Verify that a cell is on the boundary. @@ -4099,6 +4197,23 @@ def cell_polyhedron(self, cell): vertices, faces = self.cell_to_vertices_and_faces(cell) return Polyhedron(vertices, faces) + def cell_volume(self, cell): + """Compute the volume of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + + Returns + ------- + float + The volume of the cell. + + """ + vertices, faces = self.cell_to_vertices_and_faces(cell) + return abs(volume_polyhedron((vertices, faces))) + # # -------------------------------------------------------------------------- # # Boundaries # # -------------------------------------------------------------------------- diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 89e008ca77f..c654d5095c8 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -29,6 +29,7 @@ from compas.geometry import Point from compas.geometry import Polygon from compas.geometry import Polyhedron +from compas.geometry import Shape # noqa: F401 from compas.geometry import Vector from compas.geometry import add_vectors from compas.geometry import angle_points @@ -529,7 +530,7 @@ def from_polyhedron(cls, f): # type: (...) -> Mesh return cls.from_vertices_and_faces(p.vertices, p.faces) @classmethod - def from_shape(cls, shape, **kwargs): # type: (...) -> Mesh + def from_shape(cls, shape, **kwargs): # type: (Shape, dict) -> Mesh """Construct a mesh from a primitive shape. Parameters diff --git a/src/compas/datastructures/volmesh/volmesh.py b/src/compas/datastructures/volmesh/volmesh.py index f47d2643518..a934b26b63c 100644 --- a/src/compas/datastructures/volmesh/volmesh.py +++ b/src/compas/datastructures/volmesh/volmesh.py @@ -5,6 +5,13 @@ from itertools import product from random import sample +import compas + +if compas.PY2: + from collections import Mapping # type: ignore +else: + from collections.abc import Mapping + from compas.datastructures import Mesh from compas.datastructures.attributes import CellAttributeView from compas.datastructures.attributes import EdgeAttributeView @@ -38,10 +45,14 @@ from compas.tolerance import TOL -def iter_edges_from_vertices(vertices): - for i, u in enumerate(vertices): - j = (i + 1) % len(vertices) - yield u, vertices[j] +def uv_from_vertices(vertices): + for i in range(-1, len(vertices) - 1): + yield vertices[i], vertices[i + 1] + + +def uvw_from_vertices(vertices): + for i in range(-2, len(vertices) - 2): + yield vertices[i], vertices[i + 1], vertices[i + 2] class VolMesh(Datastructure): @@ -140,6 +151,7 @@ class VolMesh(Datastructure): @property def __data__(self): + # type: () -> dict _cell = {} for c in self._cell: faces = [] @@ -166,6 +178,7 @@ def __data__(self): @classmethod def __from_data__(cls, data): + # type: (dict) -> VolMesh volmesh = cls( default_vertex_attributes=data.get("default_vertex_attributes"), default_edge_attributes=data.get("default_edge_attributes"), @@ -199,6 +212,7 @@ def __from_data__(cls, data): return volmesh def __init__(self, default_vertex_attributes=None, default_edge_attributes=None, default_face_attributes=None, default_cell_attributes=None, name=None, **kwargs): # fmt: skip + # type: (dict | None, dict | None, dict | None, dict | None, str | None, dict) -> None super(VolMesh, self).__init__(kwargs, name=name) self._max_vertex = -1 self._max_face = -1 @@ -224,6 +238,7 @@ def __init__(self, default_vertex_attributes=None, default_edge_attributes=None, self.default_cell_attributes.update(default_cell_attributes) def __str__(self): + # type: () -> str tpl = "" return tpl.format( self.number_of_vertices(), @@ -246,6 +261,7 @@ def __str__(self): @classmethod def from_meshgrid(cls, dx=10, dy=None, dz=None, nx=10, ny=None, nz=None): + # type: (float, float | None, float | None, int, int | None, int | None) -> VolMesh """Construct a volmesh from a 3D meshgrid. Parameters @@ -311,6 +327,7 @@ def from_meshgrid(cls, dx=10, dy=None, dz=None, nx=10, ny=None, nz=None): @classmethod def from_obj(cls, filepath, precision=None): + # type: (str, int | None) -> VolMesh """Construct a volmesh object from the data described in an OBJ file. Parameters @@ -335,21 +352,38 @@ def from_obj(cls, filepath, precision=None): obj = OBJ(filepath, precision) vertices = obj.parser.vertices or [] # type: ignore faces = obj.parser.faces or [] # type: ignore - groups = obj.parser.groups or [] # type: ignore - cells = [] - for name in groups: - group = groups[name] - cell = [] - for item in group: - if item[0] != "f": - continue - face = faces[item[1]] - cell.append(face) - cells.append(cell) - return cls.from_vertices_and_cells(vertices, cells) + groups = obj.parser.groups or {} # type: ignore + objects = obj.parser.objects or {} # type: ignore + + if groups: + cells = [] + for name, group in groups.items(): + cell = [] + for item in group: + if item[0] != "f": + continue + face = faces[item[1]] + cell.append(face) + cells.append(cell) + return cls.from_vertices_and_cells(vertices, cells) + + if objects: + polyhedrons = [] + for name in objects: + vertex_xyz = objects[name][0] + vertex_index = {vertex: index for index, vertex in enumerate(vertex_xyz)} + vertices = list(vertex_xyz.values()) + faces = [[vertex_index[vertex] for vertex in face] for face in objects[name][1]] + polyhedron = Polyhedron(vertices, faces) + polyhedrons.append(polyhedron) + return cls.from_polyhedrons(polyhedrons) + + cell = [faces] + return cls.from_vertices_and_cells(vertices, cell) @classmethod def from_vertices_and_cells(cls, vertices, cells): + # type: (list[list[float]], list[list[list[int]]]) -> VolMesh """Construct a volmesh object from vertices and cells. Parameters @@ -371,17 +405,105 @@ def from_vertices_and_cells(cls, vertices, cells): """ volmesh = cls() - for x, y, z in vertices: - volmesh.add_vertex(x=x, y=y, z=z) + + if isinstance(vertices, Mapping): + for key, xyz in vertices.items(): + volmesh.add_vertex(key=key, attr_dict=dict(zip(("x", "y", "z"), xyz))) + else: + for x, y, z in iter(vertices): + volmesh.add_vertex(x=x, y=y, z=z) + for cell in cells: volmesh.add_cell(cell) return volmesh + @classmethod + def from_meshes(cls, meshes): + # type: (list[Mesh]) -> VolMesh + """Construct a volmesh from a list of faces. + + Parameters + ---------- + meshes : list[:class:`Mesh`] + The input meshes. + + Returns + ------- + :class:`VolMesh` + + Notes + ----- + The cycle directions of the faces of the meshes are neither checked, nor changed. + This means that the cycle directions of the provided meshes have to be consistent. + + """ + gkey_xyz = {} + cells = [] + + for mesh in meshes: + for vertex in mesh.vertices(): + xyz = mesh.vertex_attributes(vertex, "xyz") + gkey = TOL.geometric_key(xyz) + gkey_xyz[gkey] = xyz + cell = [] + for face in mesh.faces(): + temp = [] + for vertex in mesh.face_vertices(face): + xyz = mesh.vertex_attributes(vertex, "xyz") + gkey = TOL.geometric_key(xyz) + temp.append(gkey) + cell.append(temp) + cells.append(cell) + + gkey_index = {gkey: index for index, gkey in enumerate(gkey_xyz)} + vertices = list(gkey_xyz.values()) + cells = [[[gkey_index[gkey] for gkey in face] for face in cell] for cell in cells] + + return cls.from_vertices_and_cells(vertices, cells) + + @classmethod + def from_polyhedrons(cls, polyhedrons): + # type: (list[Polyhedron]) -> VolMesh + """Construct a VolMesh from a list of polyhedrons. + + Parameters + ---------- + polyhedrons : list[:class:`Polyhedron`] + + Returns + ------- + :class:`VolMesh` + + """ + gkey_xyz = {} + cells = [] + + for polyhedron in polyhedrons: + for vertex in polyhedron.vertices: + gkey = TOL.geometric_key(vertex) + gkey_xyz[gkey] = vertex + cell = [] + for face in polyhedron.faces: + temp = [] + for index in face: + xyz = polyhedron.vertices[index] + gkey = TOL.geometric_key(xyz) + temp.append(gkey) + cell.append(temp) + cells.append(cell) + + gkey_index = {gkey: index for index, gkey in enumerate(gkey_xyz)} + vertices = list(gkey_xyz.values()) + cells = [[[gkey_index[gkey] for gkey in face] for face in cell] for cell in cells] + + return cls.from_vertices_and_cells(vertices, cells) + # -------------------------------------------------------------------------- # Conversions # -------------------------------------------------------------------------- def to_obj(self, filepath, precision=None, **kwargs): + # type: (str, int | None, dict) -> None """Write the volmesh to an OBJ file. Parameters @@ -409,10 +531,12 @@ def to_obj(self, filepath, precision=None, **kwargs): the faces to the file. """ + meshes = [self.cell_to_mesh(cell) for cell in self.cells()] obj = OBJ(filepath, precision=precision) - obj.write(self, **kwargs) + obj.write(meshes, **kwargs) def to_vertices_and_cells(self): + # type: () -> tuple[list[list[float]], list[list[list[int]]]] """Return the vertices and cells of a volmesh. Returns @@ -436,6 +560,7 @@ def to_vertices_and_cells(self): return vertices, cells def cell_to_mesh(self, cell): + # type: (int) -> Mesh """Construct a mesh object from from a cell of a volmesh. Parameters @@ -457,6 +582,7 @@ def cell_to_mesh(self, cell): return Mesh.from_vertices_and_faces(vertices, faces) def cell_to_vertices_and_faces(self, cell): + # type: (int) -> tuple[list[list[float]], list[list[int]]] """Return the vertices and faces of a cell. Parameters @@ -488,6 +614,7 @@ def cell_to_vertices_and_faces(self, cell): # -------------------------------------------------------------------------- def clear(self): + # type: () -> None """Clear all the volmesh data. Returns @@ -514,6 +641,7 @@ def clear(self): self._max_cell = -1 def vertex_sample(self, size=1): + # type: (int) -> list[int] """Get the identifiers of a set of random vertices. Parameters @@ -534,6 +662,7 @@ def vertex_sample(self, size=1): return sample(list(self.vertices()), size) def edge_sample(self, size=1): + # type: (int) -> list[tuple[int, int]] """Get the identifiers of a set of random edges. Parameters @@ -554,6 +683,7 @@ def edge_sample(self, size=1): return sample(list(self.edges()), size) def face_sample(self, size=1): + # type: (int) -> list[int] """Get the identifiers of a set of random faces. Parameters @@ -574,6 +704,7 @@ def face_sample(self, size=1): return sample(list(self.faces()), size) def cell_sample(self, size=1): + # type: (int) -> list[int] """Get the identifiers of a set of random cells. Parameters @@ -594,6 +725,7 @@ def cell_sample(self, size=1): return sample(list(self.cells()), size) def vertex_index(self): + # type: () -> dict[int, int] """Returns a dictionary that maps vertex dictionary keys to the corresponding index in a vertex list or array. @@ -610,6 +742,7 @@ def vertex_index(self): return {key: index for index, key in enumerate(self.vertices())} def index_vertex(self): + # type: () -> dict[int, int] """Returns a dictionary that maps the indices of a vertex list to keys in the vertex dictionary. @@ -626,6 +759,7 @@ def index_vertex(self): return dict(enumerate(self.vertices())) def vertex_gkey(self, precision=None): + # type: (int | None) -> dict[int, str] """Returns a dictionary that maps vertex dictionary keys to the corresponding *geometric key* up to a certain precision. @@ -650,6 +784,7 @@ def vertex_gkey(self, precision=None): return {vertex: gkey(xyz(vertex), precision) for vertex in self.vertices()} def gkey_vertex(self, precision=None): + # type: (int | None) -> dict[str, int] """Returns a dictionary that maps *geometric keys* of a certain precision to the keys of the corresponding vertices. @@ -678,6 +813,7 @@ def gkey_vertex(self, precision=None): # -------------------------------------------------------------------------- def add_vertex(self, key=None, attr_dict=None, **kwattr): + # type: (int | None, dict | None, dict) -> int """Add a vertex to the volmesh object. Parameters @@ -722,6 +858,7 @@ def add_vertex(self, key=None, attr_dict=None, **kwattr): return key def add_halfface(self, vertices, fkey=None, attr_dict=None, **kwattr): + # type: (list[int], int | None, dict | None, dict) -> int """Add a face to the volmesh object. Parameters @@ -741,6 +878,11 @@ def add_halfface(self, vertices, fkey=None, attr_dict=None, **kwattr): int The key of the face. + Raises + ------ + ValueError + If the number of vertices is less than 3. + See Also -------- :meth:`add_vertex`, :meth:`add_cell` @@ -756,27 +898,33 @@ def add_halfface(self, vertices, fkey=None, attr_dict=None, **kwattr): """ if len(vertices) < 3: - return + raise ValueError("A half-face should have at least 3 vertices: {}".format(vertices)) + if vertices[-1] == vertices[0]: vertices = vertices[:-1] vertices = [int(key) for key in vertices] + if fkey is None: fkey = self._max_face = self._max_face + 1 fkey = int(fkey) if fkey > self._max_face: self._max_face = fkey + attr = attr_dict or {} attr.update(kwattr) self._halfface[fkey] = vertices + for name, value in attr.items(): self.face_attribute(fkey, name, value) - for u, v in iter_edges_from_vertices(vertices): + + for u, v, w in uvw_from_vertices(vertices): if v not in self._plane[u]: self._plane[u][v] = {} - self._plane[u][v][fkey] = None - if u not in self._plane[v]: - self._plane[v][u] = {} - self._plane[v][u][fkey] = None + self._plane[u][v][w] = None + if v not in self._plane[w]: + self._plane[w][v] = {} + if u not in self._plane[w][v]: + self._plane[w][v][u] = None return fkey @@ -823,19 +971,23 @@ def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): ckey = int(ckey) if ckey > self._max_cell: self._max_cell = ckey + attr = attr_dict or {} attr.update(kwattr) self._cell[ckey] = {} + for name, value in attr.items(): self.cell_attribute(ckey, name, value) + for vertices in faces: fkey = self.add_halfface(vertices) vertices = self.halfface_vertices(fkey) - for u, v in iter_edges_from_vertices(vertices): + + for u, v, w in uvw_from_vertices(vertices): if u not in self._cell[ckey]: self._cell[ckey][u] = {} - self._plane[u][v][fkey] = ckey self._cell[ckey][u][v] = fkey + self._plane[u][v][w] = ckey return ckey @@ -878,6 +1030,7 @@ def delete_cell(self, cell): """ cell_vertices = self.cell_vertices(cell) cell_faces = self.cell_faces(cell) + for face in cell_faces: for edge in self.halfface_halfedges(face): u, v = edge @@ -885,12 +1038,14 @@ def delete_cell(self, cell): del self._edge_data[u, v] if (v, u) in self._edge_data: del self._edge_data[v, u] + for vertex in cell_vertices: if len(self.vertex_cells(vertex)) == 1: del self._vertex[vertex] + for face in cell_faces: vertices = self.halfface_vertices(face) - for u, v in iter_edges_from_vertices(vertices): + for u, v in uv_from_vertices(vertices): self._plane[u][v][face] = None if self._plane[v][u][face] is None: del self._plane[u][v][face] @@ -899,6 +1054,7 @@ def delete_cell(self, cell): key = "-".join(map(str, sorted(vertices))) if key in self._face_data: del self._face_data[key] + del self._cell[cell] if cell in self._cell_data: del self._cell_data[cell] @@ -1547,6 +1703,22 @@ def vertex_max_degree(self): return 0 return max(self.vertex_degree(vertex) for vertex in self.vertices()) + def vertex_edges(self, vertex): + """Compute the edges connected to a given vertex. + + Parameters + ---------- + vertex : int + The vertex identifier. + + Returns + ------- + list[tuple[int, int]] + The connected edges. + + """ + return [(vertex, nbr) for nbr in sorted(self.vertex_neighbors(vertex))] + def vertex_halffaces(self, vertex): """Return all halffaces connected to a vertex. @@ -1565,15 +1737,13 @@ def vertex_halffaces(self, vertex): :meth:`vertex_neighbors`, :meth:`vertex_faces`, :meth:`vertex_cells` """ - cells = self.vertex_cells(vertex) - nbrs = self.vertex_neighbors(vertex) - halffaces = set() - for cell in cells: - for nbr in nbrs: - if nbr in self._cell[cell][vertex]: - halffaces.add(self._cell[cell][vertex][nbr]) - halffaces.add(self._cell[cell][nbr][vertex]) - return list(halffaces) + u = vertex + faces = [] + for v in self._plane[u]: + for face in self._plane[u][v]: + if face is not None: + faces.append(face) + return faces def vertex_cells(self, vertex): """Return all cells connected to a vertex. @@ -1593,12 +1763,14 @@ def vertex_cells(self, vertex): :meth:`vertex_neighbors`, :meth:`vertex_faces`, :meth:`vertex_halffaces` """ - cells = set() - for nbr in self._plane[vertex]: - for cell in self._plane[vertex][nbr].values(): + u = vertex + cells = [] + for v in self._plane[u]: + for w in self._plane[u][v]: + cell = self._plane[u][v][w] if cell is not None: - cells.add(cell) - return list(cells) + cells.append(cell) + return cells def is_vertex_on_boundary(self, vertex): """Verify that a vertex is on a boundary. @@ -1738,17 +1910,16 @@ def edges(self, data=False): """ seen = set() - for face in self._halfface: - vertices = self._halfface[face] - for u, v in pairwise(vertices + vertices[:1]): - if (u, v) in seen or (v, u) in seen: + for vertex in self.vertices(): + for nbr in sorted(self.vertex_neighbors(vertex)): + if (vertex, nbr) in seen or (nbr, vertex) in seen: continue - seen.add((u, v)) - seen.add((v, u)) + seen.add((vertex, nbr)) + seen.add((nbr, vertex)) if not data: - yield u, v + yield vertex, nbr else: - yield (u, v), self.edge_attributes((u, v)) + yield (vertex, nbr), self.edge_attributes((vertex, nbr)) def edges_where(self, conditions=None, data=False, **kwargs): """Get edges for which a certain condition or set of conditions is true. @@ -2125,18 +2296,12 @@ def edge_halffaces(self, edge): """ u, v = edge - cells = [cell for cell in self._plane[u][v].values() if cell is not None] - cell = cells[0] halffaces = [] - if self.is_edge_on_boundary(edge): - for cell in cells: - halfface = self._cell[cell][v][u] - if self.is_halfface_on_boundary(halfface): - break - for _ in cells: - halfface = self._cell[cell][u][v] - cell = self._plane[v][u][halfface] - halffaces.append(halfface) + for w in self._plane[u][v]: + cell = self._plane[u][v][w] + if cell is not None: + face = self._cell[cell][u][v] + halffaces.append(face) return halffaces def edge_cells(self, edge): @@ -2449,9 +2614,10 @@ def faces(self, data=False): faces = [] for face in self._halfface: key = "-".join(map(str, sorted(self.halfface_vertices(face)))) - if key not in seen: - seen.add(key) - faces.append(face) + if key in seen: + continue + seen.add(key) + faces.append(face) for face in faces: if not data: yield face @@ -2871,8 +3037,8 @@ def halfface_cell(self, halfface): :meth:`halfface_opposite_cell` """ - u, v = self._halfface[halfface][:2] - return self._plane[u][v][halfface] + u, v, w = self._halfface[halfface][:3] + return self._plane[u][v][w] def halfface_opposite_cell(self, halfface): """The cell to which the opposite halfface belongs to. @@ -2892,8 +3058,8 @@ def halfface_opposite_cell(self, halfface): :meth:`halfface_cell` """ - u, v = self._halfface[halfface][:2] - return self._plane[v][u][halfface] + u, v, w = self._halfface[halfface][:3] + return self._plane[w][v][u] def halfface_opposite_halfface(self, halfface): """The opposite face of a face. @@ -2918,9 +3084,9 @@ def halfface_opposite_halfface(self, halfface): For a boundary face, the opposite face is None. """ - u, v = self._halfface[halfface][:2] - nbr = self._plane[v][u][halfface] - return None if nbr is None else self._cell[nbr][v][u] + u, v, w = self._halfface[halfface][:3] + nbr = self._plane[w][v][u] + return None if nbr is None else self._cell[nbr][w][v] def halfface_adjacent_halfface(self, halfface, halfedge): """Return the halfface adjacent to the halfface across the halfedge. @@ -3713,7 +3879,7 @@ def cell_vertices(self, cell): Notes ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.vertices`, + This method is similar to :meth:`~compas.datastructures.Mesh.vertices`, but in the context of a cell of the `VolMesh`. """ @@ -3738,7 +3904,7 @@ def cell_halfedges(self, cell): Notes ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.halfedges`, + This method is similar to :meth:`~compas.datastructures.Mesh.halfedges`, but in the context of a cell of the `VolMesh`. """ @@ -3766,11 +3932,11 @@ def cell_edges(self, cell): Notes ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.edges`, + This method is similar to :meth:`~compas.datastructures.Mesh.edges`, but in the context of a cell of the `VolMesh`. """ - raise NotImplementedError + return list(set(self.cell_halfedges(cell))) def cell_faces(self, cell): """The faces of a cell. @@ -3791,7 +3957,7 @@ def cell_faces(self, cell): Notes ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.faces`, + This method is similar to :meth:`~compas.datastructures.Mesh.faces`, but in the context of a cell of the `VolMesh`. """ @@ -3823,7 +3989,7 @@ def cell_vertex_neighbors(self, cell, vertex): ----- All of the returned vertices are part of the cell. - This method is similar to :meth:`~compas.datastructures.HalfEdge.vertex_neighbors`, + This method is similar to :meth:`~compas.datastructures.Mesh.vertex_neighbors`, but in the context of a cell of the `VolMesh`. """ @@ -3861,7 +4027,7 @@ def cell_vertex_faces(self, cell, vertex): ----- All of the returned faces should are part of the same cell. - This method is similar to :meth:`~compas.datastructures.HalfEdge.vertex_faces`, + This method is similar to :meth:`~compas.datastructures.Mesh.vertex_faces`, but in the context of a cell of the `VolMesh`. """ @@ -3896,7 +4062,7 @@ def cell_halfedge_face(self, cell, halfedge): Notes ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.halfedge_face`, + This method is similar to :meth:`~compas.datastructures.Mesh.halfedge_face`, but in the context of a cell of the `VolMesh`. """ @@ -3947,7 +4113,7 @@ def cell_face_neighbors(self, cell, face): Notes ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.face_neighbors`, + This method is similar to :meth:`~compas.datastructures.Mesh.face_neighbors`, but in the context of a cell of the `VolMesh`. """ @@ -3977,10 +4143,12 @@ def cell_neighbors(self, cell): """ nbrs = [] - for face in self.cell_faces(cell): - nbr = self.halfface_opposite_cell(face) - if nbr is not None: - nbrs.append(nbr) + for u in self._cell[cell]: + for face in self._cell[cell][u].values(): + a, b, c = self._halfface[face][:3] + nbr = self._plane[c][b][a] + if nbr is not None: + nbrs.append(nbr) return nbrs def is_cell_on_boundary(self, cell): @@ -4027,11 +4195,51 @@ def cell_points(self, cell): See Also -------- - :meth:`cell_polygon`, :meth:`cell_centroid`, :meth:`cell_center` + :meth:`cell_lines`, :meth:`cell_polygons` """ return [self.vertex_point(vertex) for vertex in self.cell_vertices(cell)] + def cell_lines(self, cell): + """Compute the lines of the edges of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + + Returns + ------- + list[:class:`compas.geometry.Line`] + The lines of the edges of the cell. + + See Also + -------- + :meth:`cell_points`, :meth:`cell_polygons` + + """ + return [self.edge_line(edge) for edge in self.cell_edges(cell)] + + def cell_polygons(self, cell): + """Compute the polygons of the faces of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + + Returns + ------- + list[:class:`compas.geometry.Polygon`] + The polygons of the faces of the cell. + + See Also + -------- + :meth:`cell_points`, :meth:`cell_lines` + + """ + return [self.face_polygon(face) for face in self.cell_faces(cell)] + def cell_centroid(self, cell): """Compute the point at the centroid of a cell. diff --git a/src/compas/geometry/__init__.py b/src/compas/geometry/__init__.py index 0fe7a9a55fc..369e54a9647 100644 --- a/src/compas/geometry/__init__.py +++ b/src/compas/geometry/__init__.py @@ -385,6 +385,9 @@ from .curves.bezier import Bezier from .curves.nurbs import NurbsCurve +from .polygon import Polygon +from .polyhedron import Polyhedron + from .surfaces.surface import Surface from .surfaces.spherical import SphericalSurface from .surfaces.cylindrical import CylindricalSurface @@ -401,9 +404,6 @@ from .shapes.sphere import Sphere from .shapes.torus import Torus -from .polygon import Polygon -from .polyhedron import Polyhedron - from .brep.errors import ( BrepError, BrepInvalidError, diff --git a/src/compas/geometry/polyhedron.py b/src/compas/geometry/polyhedron.py index 908bc65eea2..b1bd501af4e 100644 --- a/src/compas/geometry/polyhedron.py +++ b/src/compas/geometry/polyhedron.py @@ -271,7 +271,7 @@ def __or__(self, other): @property def vertices(self): - if not self._vertices: + if self._vertices is None: self._vertices = [] return self._vertices @@ -281,7 +281,7 @@ def vertices(self, vertices): @property def faces(self): - if not self._faces: + if self._faces is None: self._faces = [] return self._faces diff --git a/src/compas/geometry/shapes/box.py b/src/compas/geometry/shapes/box.py index 616375bd6ac..75b38df01b7 100644 --- a/src/compas/geometry/shapes/box.py +++ b/src/compas/geometry/shapes/box.py @@ -269,28 +269,6 @@ def left(self): def top(self): return [4, 5, 6, 7] - @property - def points(self): - point = self.frame.point - xaxis = self.frame.xaxis - yaxis = self.frame.yaxis - zaxis = self.frame.zaxis - - dx = 0.5 * self.xsize - dy = 0.5 * self.ysize - dz = 0.5 * self.zsize - - a = point + xaxis * -dx + yaxis * -dy + zaxis * -dz - b = point + xaxis * -dx + yaxis * +dy + zaxis * -dz - c = point + xaxis * +dx + yaxis * +dy + zaxis * -dz - d = point + xaxis * +dx + yaxis * -dy + zaxis * -dz - e = a + zaxis * self.zsize - f = d + zaxis * self.zsize - g = c + zaxis * self.zsize - h = b + zaxis * self.zsize - - return [a, b, c, d, e, f, g, h] - # ========================================================================== # Constructors # ========================================================================== @@ -474,24 +452,11 @@ def from_points(cls, points): # type: (...) -> Box return cls.from_bounding_box(bbox) # ========================================================================== - # Conversions + # Discretisation # ========================================================================== - def to_vertices_and_faces(self, triangulated=False): - """Returns a list of vertices and faces. - - Parameters - ---------- - triangulated: bool, optional - If True, triangulate the faces. - - Returns - ------- - list[list[float]], list[list[int]] - A list of vertex locations, and a list of faces, - with each face defined as a list of indices into the list of vertices. - - """ + def compute_vertices(self): # type: () -> list[list[float]] + """Compute the vertices of the discrete representation of the box.""" point = self.frame.point xaxis = self.frame.xaxis yaxis = self.frame.yaxis @@ -510,39 +475,19 @@ def to_vertices_and_faces(self, triangulated=False): g = c + zaxis * self.zsize h = b + zaxis * self.zsize - vertices = [a, b, c, d, e, f, g, h] - _faces = [self.bottom, self.front, self.right, self.back, self.left, self.top] - - if triangulated: - faces = [] - for a, b, c, d in _faces: - faces.append([a, b, c]) - faces.append([a, c, d]) - else: - faces = _faces - - return vertices, faces - - def to_mesh(self, triangulated=False): - """Returns a mesh representation of the box. - - Parameters - ---------- - triangulated: bool, optional - If True, triangulate the faces. - - Returns - ------- - :class:`compas.datastructures.Mesh` - - """ - from compas.datastructures import Mesh + return [a, b, c, d, e, f, g, h] - vertices, faces = self.to_vertices_and_faces(triangulated=triangulated) + def compute_faces(self): # type: () -> list[list[int]] + """Compute the faces of the discrete representation of the box.""" + return [self.bottom, self.front, self.right, self.back, self.left, self.top] - mesh = Mesh.from_vertices_and_faces(vertices, faces) + def compute_edges(self): # type: () -> list[tuple[int, int]] + """Compute the faces of the discrete representation of the box.""" + return [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 7), (2, 6), (3, 5)] - return mesh + # ========================================================================== + # Conversions + # ========================================================================== def to_brep(self): """Returns a BREP representation of the box. diff --git a/src/compas/geometry/shapes/capsule.py b/src/compas/geometry/shapes/capsule.py index 4d97816b6ee..fa63f0161c4 100644 --- a/src/compas/geometry/shapes/capsule.py +++ b/src/compas/geometry/shapes/capsule.py @@ -240,40 +240,20 @@ def from_circle_and_height(cls, circle, height): # type: (...) -> Capsule return cls(frame=circle.frame, radius=circle.radius, height=height) # ============================================================================= - # Conversions + # Discretisation # ============================================================================= - def to_vertices_and_faces(self, u=16, v=16, triangulated=False): - """Returns a list of vertices and faces. - - Note that the vertex coordinates are defined with respect to the global coordinate system, - and not to the local coordinate system of the capsule. - - Parameters - ---------- - u : int, optional - Number of faces in the 'u' direction. - v : int, optional - Number of faces in the 'v' direction. - triangulated: bool, optional - If True, triangulate the faces. + def compute_vertices(self): # type: () -> list[list[float]] + """Compute the vertices of the discrete representation of the capsule. Returns ------- - list[list[float]], list[list[int]] - A list of vertex locations, and a list of faces, - with each face defined as a list of indices into the list of vertices. - - Raises - ------ - ValueError - If the value for ``u`` or ``v`` is smaller than 3. + list[list[float]] """ - if u < 3: - raise ValueError("The value for u should be u > 3.") - if v < 3: - raise ValueError("The value for v should be v > 3.") + u = self.resolution_u + v = self.resolution_v + if v % 2 == 1: v += 1 @@ -300,6 +280,25 @@ def to_vertices_and_faces(self, u=16, v=16, triangulated=False): vertices.append([0, 0, halfheight + self.radius]) vertices.append([0, 0, -halfheight - self.radius]) + vertices = transform_points(vertices, self.transformation) + return vertices + + def compute_faces(self): # type: () -> list[list[int]] + """Compute the faces of the discrete representation of the capsule. + + Returns + ------- + list[list[int]] + + """ + u = self.resolution_u + v = self.resolution_v + + if v % 2 == 1: + v += 1 + + vertices = self._vertices + faces = [] # south pole triangle fan @@ -323,29 +322,11 @@ def to_vertices_and_faces(self, u=16, v=16, triangulated=False): nn = len(vertices) - 3 - (j + 1) % u faces.append([np, nn, nc]) - if triangulated: - triangles = [] - for face in faces: - if len(face) == 4: - triangles.append(face[0:3]) - triangles.append([face[0], face[2], face[3]]) - else: - triangles.append(face) - faces = triangles - - vertices = transform_points(vertices, self.transformation) - - return vertices, faces - - def to_brep(self): - """Returns a BRep representation of the capsule. + return faces - Returns - ------- - :class:`compas.brep.Brep` - - """ - raise NotImplementedError + # ============================================================================= + # Conversions + # ============================================================================= # ============================================================================= # Transformations diff --git a/src/compas/geometry/shapes/cone.py b/src/compas/geometry/shapes/cone.py index cc5b9d49925..e923495135a 100644 --- a/src/compas/geometry/shapes/cone.py +++ b/src/compas/geometry/shapes/cone.py @@ -232,29 +232,19 @@ def from_circle_and_height(cls, circle, height): # type: (...) -> Cone frame = circle.frame return cls(frame=frame, radius=circle.radius, height=height) - # ========================================================================== - # Conversions - # ========================================================================== + # ============================================================================= + # Discretisation + # ============================================================================= - def to_vertices_and_faces(self, u=16, triangulated=False): - """Returns a list of vertices and faces. - - Parameters - ---------- - u : int, optional - Number of faces in the "u" direction. - triangulated: bool, optional - If True, triangulate the faces. + def compute_vertices(self): # type: () -> list[list[float]] + """Compute the vertices of the discrete representation of the cone. Returns ------- - list[list[float]], list[list[int]] - A list of vertex locations, and a list of faces, - with each face defined as a list of indices into the list of vertices. + list[list[float]] """ - if u < 3: - raise ValueError("The value for u should be u > 3.") + u = self.resolution_u vertices = [[0, 0, 0]] a = 2 * pi / u @@ -263,7 +253,20 @@ def to_vertices_and_faces(self, u=16, triangulated=False): x = radius * cos(i * a) y = radius * sin(i * a) vertices.append([x, y, 0]) # type: ignore - vertices.append([0, 0, self.height]) # type: ignore + vertices.append([0, 0, self.height]) + + vertices = transform_points(vertices, self.transformation) + return vertices + + def compute_faces(self): # type: () -> list[list[int]] + """Compute the faces of the discrete representation of the cone. + + Returns + ------- + list[list[int]] + + """ + vertices = self._vertices faces = [] first = 0 @@ -274,19 +277,11 @@ def to_vertices_and_faces(self, u=16, triangulated=False): faces.append([last - 1, 1, last]) faces.append([1, last - 1, first]) - if triangulated: - triangles = [] - for face in faces: - if len(face) == 4: - triangles.append(face[0:3]) - triangles.append([face[0], face[2], face[3]]) - else: - triangles.append(face) - faces = triangles + return faces - vertices = transform_points(vertices, self.transformation) - - return vertices, faces + # ========================================================================== + # Conversions + # ========================================================================== def to_brep(self): """Returns a BRep representation of the cone. diff --git a/src/compas/geometry/shapes/cylinder.py b/src/compas/geometry/shapes/cylinder.py index cf1d8631b4c..39170e0a410 100644 --- a/src/compas/geometry/shapes/cylinder.py +++ b/src/compas/geometry/shapes/cylinder.py @@ -229,30 +229,18 @@ def from_circle_and_height(cls, circle, height): # type: (...) -> Cylinder return cls(frame=circle.frame, height=height, radius=circle.radius) # ============================================================================= - # Conversions + # Discretisation # ============================================================================= - def to_vertices_and_faces(self, u=16, triangulated=False): - """Returns a list of vertices and faces. - - Parameters - ---------- - u : int, optional - Number of faces in the "u" direction. - triangulated: bool, optional - If True, triangulate the faces. + def compute_vertices(self): # type: () -> list[list[float]] + """Compute the vertices of the discrete representation of the cylinder. Returns ------- list[list[float]] - A list of vertex locations. - list[list[int]] - And a list of faces, - with each face defined as a list of indices into the list of vertices. """ - if u < 3: - raise ValueError("The value for u should be u > 3.") + u = self.resolution_u vertices = [] a = 2 * pi / u @@ -266,6 +254,21 @@ def to_vertices_and_faces(self, u=16, triangulated=False): vertices.append([0, 0, z]) vertices.append([0, 0, -z]) + vertices = transform_points(vertices, self.transformation) + return vertices + + def compute_faces(self): # type: () -> list[list[int]] + """Compute the faces of the discrete representation of the cylinder. + + Returns + ------- + list[list[int]] + + """ + u = self.resolution_u + + vertices = self._vertices + faces = [] # side faces for i in range(0, u * 2, 2): @@ -277,19 +280,11 @@ def to_vertices_and_faces(self, u=16, triangulated=False): faces.append(top) faces.append(bottom[::-1]) - if triangulated: - triangles = [] - for face in faces: - if len(face) == 4: - triangles.append(face[0:3]) - triangles.append([face[0], face[2], face[3]]) - else: - triangles.append(face) - faces = triangles + return faces - vertices = transform_points(vertices, self.transformation) - - return vertices, faces + # ============================================================================= + # Conversions + # ============================================================================= def to_brep(self): """Returns a BRep representation of the cylinder. diff --git a/src/compas/geometry/shapes/shape.py b/src/compas/geometry/shapes/shape.py index 1969885b94f..dd3cf35137e 100644 --- a/src/compas/geometry/shapes/shape.py +++ b/src/compas/geometry/shapes/shape.py @@ -2,10 +2,22 @@ from __future__ import division from __future__ import print_function +import compas # noqa: F401 from compas.geometry import Frame from compas.geometry import Geometry +from compas.geometry import Line +from compas.geometry import Point +from compas.geometry import Polygon from compas.geometry import Rotation from compas.geometry import Transformation +from compas.itertools import pairwise + +if not compas.IPY: + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + import compas.datastructures # noqa: F401 + import compas.geometry # noqa: F401 class Shape(Geometry): @@ -43,11 +55,17 @@ class Shape(Geometry): """ - def __init__(self, frame=None, name=None): + def __init__(self, frame=None, name=None): # type: (Frame | None, str | None) -> None super(Shape, self).__init__(name=name) self._frame = None self._transformation = None self.frame = frame + self._resolution_u = 16 + self._resolution_v = 16 + self._vertices = None + self._edges = None + self._faces = None + self._triangles = None # ============================================================================= # Data @@ -58,13 +76,13 @@ def __init__(self, frame=None, name=None): # ============================================================================= @property - def frame(self): + def frame(self): # type: () -> Frame if not self._frame: self._frame = Frame.worldXY() return self._frame @frame.setter - def frame(self, frame): + def frame(self, frame): # type: (Frame) -> None if not frame: self._frame = None else: @@ -72,30 +90,155 @@ def frame(self, frame): self._transformation = None @property - def transformation(self): + def transformation(self): # type: () -> Transformation if not self._transformation: self._transformation = Transformation.from_frame_to_frame(Frame.worldXY(), self.frame) return self._transformation @property - def area(self): + def area(self): # type: () -> float raise NotImplementedError @property - def volume(self): + def volume(self): # type: () -> float raise NotImplementedError + @property + def resolution_u(self): # type: () -> int + return self._resolution_u + + @resolution_u.setter + def resolution_u(self, u): # type: (int) -> None + if u < 3: + raise ValueError("The value for u should be u > 3.") + self._resolution_u = u + self._vertices = None + self._edges = None + self._faces = None + self._triangles = None + + @property + def resolution_v(self): # type: () -> int + return self._resolution_v + + @resolution_v.setter + def resolution_v(self, v): # type: (int) -> None + if v < 3: + raise ValueError("The value for v should be v > 3.") + self._resolution_v = v + self._vertices = None + self._edges = None + self._faces = None + self._triangles = None + + @property + def vertices(self): # type: () -> list[list[float]] + self._vertices = self.compute_vertices() + return self._vertices + + @property + def edges(self): # type: () -> list[tuple[int, int]] + if not self._edges: + self._edges = self.compute_edges() + return self._edges + + @property + def faces(self): # type: () -> list[list[int]] + if not self._faces: + self._faces = self.compute_faces() + return self._faces + + @property + def triangles(self): # type: () -> list[tuple[int, int, int]] + if not self._triangles: + self._triangles = self.compute_triangles() + return self._triangles + + @property + def points(self): # type: () -> list[Point] + vertices = self.compute_vertices() + return [Point(x, y, z) for x, y, z in vertices] + + @property + def lines(self): # type: () -> list[Line] + vertices = self.compute_vertices() + return [Line(vertices[u], vertices[v]) for u, v in self.edges] + + @property + def polygons(self): # type: () -> list[Polygon] + vertices = self.compute_vertices() + return [[Polygon([vertices[v] for v in face])] for face in self.faces] + # ============================================================================= # Constructors # ============================================================================= + # ============================================================================= + # Discretisation + # ============================================================================= + + def compute_vertices(self): + raise NotImplementedError + + def compute_faces(self): + raise NotImplementedError + + def compute_edges(self): # type: () -> list[tuple[int, int]] + """Compute the edges of the discrete representation of the shape. + + Returns + ------- + list[tuple[int, int]] + + """ + edges = [] + seen = set() + for face in self.faces: + for u, v in pairwise(face + face[:1]): + if (u, v) not in seen: + seen.add((u, v)) + seen.add((v, u)) + edges.append((u, v)) + + return edges + + def compute_triangles(self): # type: () -> list[tuple[int, int, int]] + """Compute the triangles of the discrete representation of the shape. + + Returns + ------- + list[tuple[int, int, int]] + + """ + triangles = [] + for face in self.faces: + if len(face) == 4: + a, b, c, d = face + triangles.append((a, b, c)) + triangles.append((a, c, d)) + else: + triangles.append(face) + return triangles + # ============================================================================= # Conversions # ============================================================================= - def to_vertices_and_faces(self, **kwargs): + def to_vertices_and_faces(self, triangulated=False, u=None, v=None): + # type: (bool, int | None, int | None) -> tuple[list[list[float]], list[list[int]]] """Convert the shape to a list of vertices and faces. + Parameters + ---------- + triangulated : bool, optional + If True, triangulate the faces. + u : int, optional + Number of faces in the "u" direction. + If no value is provided, the value of `self.resolution_u` will be used. + v : int, optional + Number of faces in the "v" direction. + If no value is provided, the value of `self.resolution_v` will be used. + Returns ------- list of list of float @@ -104,9 +247,19 @@ def to_vertices_and_faces(self, **kwargs): The faces of the shape. """ - raise NotImplementedError + if u: + self.resolution_u = u + if v: + self.resolution_v = v + vertices = self.vertices + if triangulated: + faces = self.triangles + else: + faces = self.faces + return vertices, faces - def to_polyhedron(self, triangulated=True, u=16, v=None): + def to_polyhedron(self, triangulated=False, u=None, v=None): + # type: (bool, int | None, int | None) -> compas.geometry.Polyhedron """Convert the shape to a polyhedron. Parameters @@ -115,9 +268,10 @@ def to_polyhedron(self, triangulated=True, u=16, v=None): If True, triangulate the faces. u : int, optional Number of faces in the "u" direction. + If no value is provided, the value of `self.resolution_u` will be used. v : int, optional Number of faces in the "v" direction. - If no value is provided, and the shape has two parameter directions, the value of ``u`` will be used. + If no value is provided, the value of `self.resolution_v` will be used. Returns ------- @@ -135,21 +289,45 @@ def to_polyhedron(self, triangulated=True, u=16, v=None): """ from compas.geometry import Polyhedron - v = v or u + vertices, faces = self.to_vertices_and_faces(u=u, v=v, triangulated=triangulated) - vertices, faces = self.to_vertices_and_faces(u=u, v=v) + return Polyhedron(vertices, faces) - if triangulated: - triangles = [] - for face in faces: - if len(face) == 4: - triangles.append(face[0:3]) - triangles.append([face[0], face[2], face[3]]) - else: - triangles.append(face) - faces = triangles + def to_mesh(self, triangulated=False, u=None, v=None): + # type: (bool, int | None, int | None) -> compas.datastructures.Mesh + """Returns a mesh representation of the box. - return Polyhedron(vertices, faces) + Parameters + ---------- + triangulated: bool, optional + If True, triangulate the faces. + u : int, optional + Number of faces in the "u" direction. + If no value is provided, the value of `self.resolution_u` will be used. + v : int, optional + Number of faces in the "v" direction. + If no value is provided, the value of `self.resolution_v` will be used. + + Returns + ------- + :class:`compas.datastructures.Mesh` + + Notes + ----- + Parameters ``u`` and ``v`` define the resolution of the discretisation of curved geometry. + If the geometry is not curved in a particular direction, the corresponding parameter will be ignored. + For example, a cylinder has a resolution in the "u" direction, but not in the "v" direction. + A sphere has a resolution in both the "u" and the "v" direction. + A box has no resolution in either direction. + + """ + from compas.datastructures import Mesh + + vertices, faces = self.to_vertices_and_faces(u=u, v=v, triangulated=triangulated) + + mesh = Mesh.from_vertices_and_faces(vertices, faces) + + return mesh def to_brep(self): """Convert the shape to a Brep. diff --git a/src/compas/geometry/shapes/sphere.py b/src/compas/geometry/shapes/sphere.py index b0046f02362..3dba8859a1d 100644 --- a/src/compas/geometry/shapes/sphere.py +++ b/src/compas/geometry/shapes/sphere.py @@ -173,36 +173,19 @@ def from_point_and_radius(cls, point, radius): # type: (...) -> Sphere return cls(frame=frame, radius=radius) # ========================================================================== - # Conversions + # Discretisation # ========================================================================== - def to_vertices_and_faces(self, u=16, v=16, triangulated=False): - """Returns a list of vertices and faces - - The vertex positions are in world coordinates. - - Parameters - ---------- - u : int, optional - Number of faces in the "u" direction. - v : int, optional - Number of faces in the "v" direction. - triangulated: bool, optional - If True, triangulate the faces. + def compute_vertices(self): # type: () -> list[float] + """Compute the vertices of the discrete representation of the sphere. Returns ------- list[list[float]] - A list of vertex locations. - list[list[int]] - And a list of faces, - with each face defined as a list of indices into the list of vertices. """ - if u < 3: - raise ValueError("The value for u should be u > 3.") - if v < 3: - raise ValueError("The value for v should be v > 3.") + u = self.resolution_u + v = self.resolution_v theta = pi / v phi = pi * 2 / u @@ -221,6 +204,22 @@ def to_vertices_and_faces(self, u=16, v=16, triangulated=False): vertices.append([x, y, z + self.radius]) vertices.append([x, y, z - self.radius]) + vertices = transform_points(vertices, self.transformation) + return vertices + + def compute_faces(self): # type: () -> list[list[int]] + """Compute the faces of the discrete representation of the sphere. + + Returns + ------- + list[list[int]] + + """ + u = self.resolution_u + v = self.resolution_v + + vertices = self._vertices + faces = [] # south pole triangle fan @@ -244,20 +243,11 @@ def to_vertices_and_faces(self, u=16, v=16, triangulated=False): nn = len(vertices) - 3 - (j + 1) % u faces.append([np, nn, nc]) - if triangulated: - triangles = [] - for face in faces: - if len(face) == 4: - triangles.append(face[0:3]) - triangles.append([face[0], face[2], face[3]]) - else: - triangles.append(face) - faces = triangles - - # transform the vertices to world coordinates - vertices = transform_points(vertices, self.transformation) + return faces - return vertices, faces + # ========================================================================== + # Conversions + # ========================================================================== def to_brep(self): """Returns a BRep representation of the sphere. @@ -275,36 +265,6 @@ def to_brep(self): # Transformations # ============================================================================= - # def transform(self, transformation): - # """Transform the sphere. - - # Parameters - # ---------- - # transformation : :class:`compas.geometry.Transformation` - # The transformation used to transform the Sphere. - # Note that non-similarity preserving transformations will not change - # the sphere into an ellipsoid. In such case, the radius of the sphere - # will be scaled by the largest scale factor of the threee axis. - - # Returns - # ------- - # None - - # Examples - # -------- - # >>> from compas.geometry import Frame - # >>> from compas.geometry import Transformation - # >>> from compas.geometry import Sphere - # >>> sphere = Sphere(Point(1, 1, 1), 5) - # >>> frame = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15]) - # >>> T = Transformation.from_frame(frame) - # >>> sphere.transform(T) - - # """ - # self.frame.transform(transformation) - # Sc, _, _, _, _ = transformation.decomposed() - # self.radius *= max([Sc[0, 0], Sc[1, 1], Sc[2, 2]]) - def scale(self, factor): """Scale the sphere. diff --git a/src/compas/geometry/shapes/torus.py b/src/compas/geometry/shapes/torus.py index 8b505c5d3a5..05188f7f0cd 100644 --- a/src/compas/geometry/shapes/torus.py +++ b/src/compas/geometry/shapes/torus.py @@ -8,7 +8,6 @@ from compas.geometry import Frame from compas.geometry import Plane -from compas.geometry import matrix_from_frame from compas.geometry import transform_points from .shape import Shape @@ -183,37 +182,23 @@ def from_plane_and_radii(cls, plane, radius_axis, radius_pipe): return cls(radius_axis=radius_axis, radius_pipe=radius_pipe, frame=frame) # ========================================================================== - # Conversions + # Discretisation # ========================================================================== - def to_vertices_and_faces(self, u=16, v=16, triangulated=False): - """Returns a list of vertices and faces. - - The vertices are defined in global coordinates. - - Parameters - ---------- - u : int, optional - Number of faces in the "u" direction. - v : int, optional - Number of faces in the "v" direction. - triangulated: bool, optional - If True, triangulate the faces. + def compute_vertices(self): # type: () -> list[float] + """Compute the vertices of the discrete representation of the sphere. Returns ------- - list[list[float]], list[list[int]] - A list of vertex locations, and a list of faces, - with each face defined as a list of indices into the list of vertices. + list[list[float]] """ - if u < 3: - raise ValueError("The value for u should be u > 3.") - if v < 3: - raise ValueError("The value for v should be v > 3.") + u = self.resolution_u + v = self.resolution_v theta = pi * 2 / u phi = pi * 2 / v + vertices = [] for i in range(u): for j in range(v): @@ -222,10 +207,19 @@ def to_vertices_and_faces(self, u=16, v=16, triangulated=False): z = self.radius_pipe * sin(j * phi) vertices.append([x, y, z]) - # transform vertices to torus' plane - frame = Frame.from_plane(self.plane) - M = matrix_from_frame(frame) - vertices = transform_points(vertices, M) + vertices = transform_points(vertices, self.transformation) + return vertices + + def compute_faces(self): # type: () -> list[list[int]] + """Compute the faces of the discrete representation of the sphere. + + Returns + ------- + list[list[int]] + + """ + u = self.resolution_u + v = self.resolution_v faces = [] for i in range(u): @@ -238,19 +232,11 @@ def to_vertices_and_faces(self, u=16, v=16, triangulated=False): d = i * v + jj faces.append([a, b, c, d]) - if triangulated: - triangles = [] - for face in faces: - if len(face) == 4: - triangles.append(face[0:3]) - triangles.append([face[0], face[2], face[3]]) - else: - triangles.append(face) - faces = triangles + return faces - vertices = transform_points(vertices, self.transformation) - - return vertices, faces + # ========================================================================== + # Conversions + # ========================================================================== def to_brep(self): """Returns a BRep representation of the torus. diff --git a/src/compas/geometry/surfaces/spherical.py b/src/compas/geometry/surfaces/spherical.py index 555ee51031f..1530b776f3a 100644 --- a/src/compas/geometry/surfaces/spherical.py +++ b/src/compas/geometry/surfaces/spherical.py @@ -220,8 +220,8 @@ def point_at(self, u, v, world=True): A point on the sphere. """ - u = u * pi - v = v * PI2 + u = u * PI2 + v = v * pi x = self.radius * cos(u) * sin(v) y = self.radius * sin(u) * sin(v) z = self.radius * cos(v) @@ -248,8 +248,8 @@ def normal_at(self, u, v, world=True): The normal vector. """ - u = u * pi - v = v * PI2 + u = u * PI2 + v = v * pi x = cos(u) * sin(v) y = sin(u) * sin(v) z = cos(v) diff --git a/src/compas/geometry/surfaces/surface.py b/src/compas/geometry/surfaces/surface.py index 4777705279b..b8b77ac75a5 100644 --- a/src/compas/geometry/surfaces/surface.py +++ b/src/compas/geometry/surfaces/surface.py @@ -282,7 +282,6 @@ def to_triangles(self, nu=16, nv=16, du=None, dv=None): The subset of the domain in the v direction. Default is ``None``, in which case the entire domain is used. - Returns ------- list[list[:class:`compas.geometry.Point`]] @@ -313,7 +312,6 @@ def to_quads(self, nu=16, nv=16, du=None, dv=None): The subset of the domain in the v direction. Default is ``None``, in which case the entire domain is used. - Returns ------- list[list[:class:`compas.geometry.Point`]] @@ -383,16 +381,6 @@ def to_mesh(self, nu=16, nv=16, du=None, dv=None): vertices, faces = self.to_vertices_and_faces(nu=nu, nv=nv, du=du, dv=dv) return Mesh.from_vertices_and_faces(vertices, faces) - def to_tesselation(self): - """Convert the surface to a triangle mesh. - - Returns - ------- - :class:`compas.datastructures.Mesh` - - """ - raise NotImplementedError - def to_brep(self): """Convert the surface to a BREP representation. diff --git a/src/compas/geometry/vector.py b/src/compas/geometry/vector.py index 8e1b5702257..7ccf054431f 100644 --- a/src/compas/geometry/vector.py +++ b/src/compas/geometry/vector.py @@ -148,11 +148,25 @@ def __add__(self, other): def __sub__(self, other): return Vector(self.x - other[0], self.y - other[1], self.z - other[2]) - def __mul__(self, n): - return Vector(self.x * n, self.y * n, self.z * n) - - def __truediv__(self, n): - return Vector(self.x / n, self.y / n, self.z / n) + def __mul__(self, other): + if isinstance(other, (int, float)): + return Vector(self.x * other, self.y * other, self.z * other) + + try: + other = Vector(*other) + return Vector(self.x * other.x, self.y * other.y, self.z * other.z) + except TypeError: + raise TypeError("Cannot cast {} {} to Vector".format(other, type(other))) + + def __truediv__(self, other): + if isinstance(other, (int, float)): + return Vector(self.x / other, self.y / other, self.z / other) + + try: + other = Vector(*other) + return Vector(self.x / other.x, self.y / other.y, self.z / other.z) + except TypeError: + raise TypeError("Cannot cast {} {} to Vector".format(other, type(other))) def __pow__(self, n): return Vector(self.x**n, self.y**n, self.z**n) @@ -190,6 +204,26 @@ def __ipow__(self, n): self.z **= n return self + def __rmul__(self, n): + return self.__mul__(n) + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + try: + other = Vector(*other) + return other - self + except TypeError: + raise TypeError("Cannot cast {} {} to Vector".format(other, type(other))) + + def __rtruediv__(self, other): + try: + other = Vector(*other) + return other / self + except TypeError: + raise TypeError("Cannot cast {} {} to Vector".format(other, type(other))) + # ========================================================================== # Properties # ========================================================================== diff --git a/src/compas/itertools.py b/src/compas/itertools.py index 97bddf5ca67..5c0a4fa6d62 100644 --- a/src/compas/itertools.py +++ b/src/compas/itertools.py @@ -14,7 +14,19 @@ try: from itertools import zip_longest except ImportError: - from itertools import izip_longest as zip_longest + from itertools import izip_longest as zip_longest # type: ignore + +__all__ = [ + "normalize_values", + "remap_values", + "meshgrid", + "linspace", + "flatten", + "reshape", + "pairwise", + "window", + "iterable_like", +] def normalize_values(values, new_min=0.0, new_max=1.0): diff --git a/src/compas/scene/assemblyobject.py b/src/compas/scene/assemblyobject.py deleted file mode 100644 index 878841c1840..00000000000 --- a/src/compas/scene/assemblyobject.py +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function diff --git a/src/compas/scene/curveobject.py b/src/compas/scene/curveobject.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/compas/scene/descriptors/color.py b/src/compas/scene/descriptors/color.py index 3a3e1014e88..aac747057fe 100644 --- a/src/compas/scene/descriptors/color.py +++ b/src/compas/scene/descriptors/color.py @@ -14,7 +14,6 @@ class ColorAttribute(object): def __init__(self, default=None, **kwargs): super(ColorAttribute, self).__init__(**kwargs) - default = default or Color.black() self.default = Color.coerce(default) def __set_name__(self, owner, name): diff --git a/src/compas/scene/frameobject.py b/src/compas/scene/frameobject.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/compas/scene/geometryobject.py b/src/compas/scene/geometryobject.py index 45ffbd8336e..b8af998d2de 100644 --- a/src/compas/scene/geometryobject.py +++ b/src/compas/scene/geometryobject.py @@ -2,6 +2,9 @@ from __future__ import division from __future__ import print_function +import compas.colors # noqa: F401 +import compas.geometry # noqa: F401 + from .descriptors.color import ColorAttribute from .sceneobject import SceneObject @@ -41,18 +44,30 @@ class GeometryObject(SceneObject): linecolor = ColorAttribute() surfacecolor = ColorAttribute() - def __init__(self, geometry, **kwargs): - super(GeometryObject, self).__init__(item=geometry, **kwargs) - self.geometry = geometry - self.pointcolor = kwargs.get("pointcolor", self.color) - self.linecolor = kwargs.get("linecolor", self.color) - self.surfacecolor = kwargs.get("surfacecolor", self.color) - self.pointsize = kwargs.get("pointsize", 1.0) - self.linewidth = kwargs.get("linewidth", 1.0) - self.show_points = kwargs.get("show_points", False) - self.show_lines = kwargs.get("show_lines", True) - self.show_surfaces = kwargs.get("show_surfaces", True) - - def draw(self): - """Draw the geometry. Implemented by child classes.""" - raise NotImplementedError + def __init__( + self, + pointcolor=None, # type: compas.colors.Color | None + linecolor=None, # type: compas.colors.Color | None + surfacecolor=None, # type: compas.colors.Color | None + pointsize=1.0, # type: float + linewidth=1.0, # type: float + show_points=False, # type: bool + show_lines=True, # type: bool + show_surfaces=True, # type: bool + **kwargs # type: dict + ): # fmt: skip + # type: (...) -> None + super(GeometryObject, self).__init__(**kwargs) # type: ignore + self.pointcolor = pointcolor or self.color + self.linecolor = linecolor or self.color + self.surfacecolor = surfacecolor or self.color + self.pointsize = pointsize + self.linewidth = linewidth + self.show_points = show_points + self.show_lines = show_lines + self.show_surfaces = show_surfaces + + @property + def geometry(self): + # type: () -> compas.geometry.Geometry + return self.item # type: ignore diff --git a/src/compas/scene/graphobject.py b/src/compas/scene/graphobject.py index bc1651dfc3d..866cdd668c8 100644 --- a/src/compas/scene/graphobject.py +++ b/src/compas/scene/graphobject.py @@ -2,6 +2,9 @@ from __future__ import division from __future__ import print_function +import compas.colors # noqa: F401 +import compas.datastructures # noqa: F401 +import compas.geometry # noqa: F401 from compas.geometry import transform_points from .descriptors.colordict import ColorDictAttribute @@ -23,6 +26,10 @@ class GraphObject(SceneObject): 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 graph. + show_nodes : Union[bool, sequence[hashable]] + Flag for showing or hiding the nodes. Default is ``True``. + show_edges : Union[bool, sequence[tuple[hashable, hashable]]] + Flag for showing or hiding the edges. Default is ``True``. nodecolor : :class:`compas.colors.ColorDict` Mapping between nodes and RGB color values. edgecolor : :class:`compas.colors.ColorDict` @@ -31,10 +38,6 @@ class GraphObject(SceneObject): The size of the nodes. Default is ``1.0``. edgewidth : float The width of the edges. Default is ``1.0``. - show_nodes : Union[bool, sequence[float]] - Flag for showing or hiding the nodes. Default is ``True``. - show_edges : Union[bool, sequence[tuple[hashable, hashable]]] - Flag for showing or hiding the edges. Default is ``True``. See Also -------- @@ -46,64 +49,84 @@ class GraphObject(SceneObject): nodecolor = ColorDictAttribute() edgecolor = ColorDictAttribute() - def __init__(self, graph, **kwargs): - super(GraphObject, self).__init__(item=graph, **kwargs) - self._graph = None + def __init__( + self, + show_nodes=True, # type: bool | list + show_edges=True, # type: bool | list + nodecolor=None, # type: dict | compas.colors.Color | None + edgecolor=None, # type: dict | compas.colors.Color | None + nodesize=1.0, # type: float + edgewidth=1.0, # type: float + **kwargs # type: dict + ): # fmt: skip + # type: (...) -> None + super(GraphObject, self).__init__(**kwargs) # type: ignore self._node_xyz = None - self.graph = graph - self.nodecolor = kwargs.get("nodecolor", self.color) - self.edgecolor = kwargs.get("edgecolor", self.color) - self.nodesize = kwargs.get("nodesize", 1.0) - self.edgewidth = kwargs.get("edgewidth", 1.0) - self.show_nodes = kwargs.get("show_nodes", True) - self.show_edges = kwargs.get("show_edges", True) + self.show_nodes = show_nodes + self.show_edges = show_edges + self.nodecolor = nodecolor or self.color + self.edgecolor = edgecolor or self.color + self.nodesize = nodesize + self.edgewidth = edgewidth + + @property + def settings(self): + # type: () -> dict + settings = super(GraphObject, self).settings + settings["show_nodes"] = self.show_nodes + settings["show_edges"] = self.show_edges + settings["nodecolor"] = self.nodecolor + settings["edgecolor"] = self.edgecolor + settings["nodesize"] = self.nodesize + settings["edgewidth"] = self.edgewidth + return settings @property def graph(self): - return self._graph + # type: () -> compas.datastructures.Graph + return self.item # type: ignore @graph.setter def graph(self, graph): - self._graph = graph + # type: (compas.datastructures.Graph) -> None + self._item = graph self._transformation = None self._node_xyz = None @property def transformation(self): + # type: () -> compas.geometry.Transformation | None return self._transformation @transformation.setter def transformation(self, transformation): + # type: (compas.geometry.Transformation) -> None self._node_xyz = None self._transformation = transformation @property def node_xyz(self): - if self._node_xyz is None: - points = self.graph.nodes_attributes("xyz") # type: ignore - points = transform_points(points, self.worldtransformation) - self._node_xyz = dict(zip(self.graph.nodes(), points)) # type: ignore - return self._node_xyz + # type: () -> dict[int | str, list[float]] + if self.graph: + if self._node_xyz is None: + points = self.graph.nodes_attributes("xyz") + points = transform_points(points, self.worldtransformation) + self._node_xyz = dict(zip(self.graph.nodes(), points)) + return self._node_xyz # type: ignore @node_xyz.setter def node_xyz(self, node_xyz): + # type: (dict[int | str, list[float]]) -> None self._node_xyz = node_xyz - def draw_nodes(self, nodes=None, color=None, text=None): + def draw_nodes(self): """Draw the nodes of the graph. - Parameters - ---------- - nodes : list[int], optional - The nodes to include in the drawing. - Default is all nodes. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[int, tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the nodes, - as either a single color to be applied to all nodes, - or a color dict, mapping specific nodes to specific colors. - text : dict[int, str], optional - The text labels for the nodes - as a text dict, mapping specific nodes to specific text labels. + Nodes are drawn based on the values of + + * `self.show_nodes` + * `self.nodecolor` + * `self.nodesize` Returns ------- @@ -113,21 +136,14 @@ def draw_nodes(self, nodes=None, color=None, text=None): """ raise NotImplementedError - def draw_edges(self, edges=None, color=None, text=None): + def draw_edges(self): """Draw the edges of the graph. - Parameters - ---------- - edges : list[tuple[int, int]], optional - The edges to include in the drawing. - Default is all edges. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[tuple[int, int], tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the edges, - as either a single color to be applied to all edges, - or a color dict, mapping specific edges to specific colors. - text : dict[tuple[int, int]], optional - The text labels for the edges - as a text dict, mapping specific edges to specific text labels. + Edges are drawn based on the values of + + * `self.show_edges` + * `self.edgecolor` + * `self.edgewidth` Returns ------- @@ -137,10 +153,6 @@ def draw_edges(self, edges=None, color=None, text=None): """ raise NotImplementedError - def draw(self): - """Draw the network.""" - raise NotImplementedError - def clear_nodes(self): """Clear the nodes of the graph. diff --git a/src/compas/scene/meshobject.py b/src/compas/scene/meshobject.py index 715e24477f8..33069b8edd2 100644 --- a/src/compas/scene/meshobject.py +++ b/src/compas/scene/meshobject.py @@ -2,6 +2,9 @@ from __future__ import division from __future__ import print_function +import compas.colors # noqa: F401 +import compas.datastructures # noqa: F401 +import compas.geometry # noqa: F401 from compas.geometry import transform_points from .descriptors.colordict import ColorDictAttribute @@ -23,8 +26,15 @@ class MeshObject(SceneObject): vertex_xyz : dict[int, list[float]] View coordinates of the vertices. Defaults to the real coordinates. - color : :class:`compas.colors.Color` - The base RGB color of the mesh. + show_vertices : Union[bool, sequence[float]] + Flag for showing or hiding the vertices, or a list of keys for the vertices to show. + Default is ``False``. + show_edges : Union[bool, sequence[tuple[int, int]]] + Flag for showing or hiding the edges, or a list of keys for the edges to show. + Default is ``True``. + show_faces : Union[bool, sequence[int]] + Flag for showing or hiding the faces, or a list of keys for the faces to show. + Default is ``True``. vertexcolor : :class:`compas.colors.ColorDict` Vertex colors. edgecolor : :class:`compas.colors.ColorDict` @@ -35,15 +45,6 @@ class MeshObject(SceneObject): The size of the vertices. Default is ``1.0``. edgewidth : float The width of the edges. Default is ``1.0``. - show_vertices : Union[bool, sequence[float]] - Flag for showing or hiding the vertices, or a list of keys for the vertices to show. - Default is ``False``. - show_edges : Union[bool, sequence[tuple[int, int]]] - Flag for showing or hiding the edges, or a list of keys for the edges to show. - Default is ``True``. - show_faces : Union[bool, sequence[int]] - Flag for showing or hiding the faces, or a list of keys for the faces to show. - Default is ``True``. See Also -------- @@ -56,66 +57,93 @@ class MeshObject(SceneObject): edgecolor = ColorDictAttribute() facecolor = ColorDictAttribute() - def __init__(self, mesh, **kwargs): - super(MeshObject, self).__init__(item=mesh, **kwargs) - self._mesh = None + def __init__( + self, + show_vertices=False, # type: bool | list + show_edges=False, # type: bool | list + show_faces=True, # type: bool | list + vertexcolor=None, # type: dict | compas.colors.Color | None + edgecolor=None, # type: dict | compas.colors.Color | None + facecolor=None, # type: dict | compas.colors.Color | None + vertexsize=1.0, # type: float + edgewidth=1.0, # type: float + **kwargs # dict + ): # fmt: skip + # type: (...) -> None + super(MeshObject, self).__init__(**kwargs) # type: ignore self._vertex_xyz = None - self.mesh = mesh - self.vertexcolor = kwargs.get("vertexcolor", self.contrastcolor) - self.edgecolor = kwargs.get("edgecolor", self.contrastcolor) - self.facecolor = kwargs.get("facecolor", self.color) - self.vertexsize = kwargs.get("vertexsize", 1.0) - self.edgewidth = kwargs.get("edgewidth", 1.0) - self.show_vertices = kwargs.get("show_vertices", False) - self.show_edges = kwargs.get("show_edges", False) - self.show_faces = kwargs.get("show_faces", True) + self.show_vertices = show_vertices + self.show_edges = show_edges + self.show_faces = show_faces + self.vertexcolor = vertexcolor or self.contrastcolor + self.edgecolor = edgecolor or self.contrastcolor + self.facecolor = facecolor or self.color + self.vertexsize = vertexsize + self.edgewidth = edgewidth + + @property + def settings(self): + # type: () -> dict + settings = super(MeshObject, self).settings + # perhaps this should be renamed to just "vertices", "edges", "faces" + settings["show_vertices"] = self.show_vertices + settings["show_edges"] = self.show_edges + settings["show_faces"] = self.show_faces + # end of perhaps + settings["vertexcolor"] = self.vertexcolor + settings["edgecolor"] = self.edgecolor + settings["facecolor"] = self.facecolor + settings["vertexsize"] = self.vertexsize + settings["edgewidth"] = self.edgewidth + return settings @property def mesh(self): - return self._mesh + # type: () -> compas.datastructures.Mesh + return self.item # type: ignore @mesh.setter def mesh(self, mesh): - self._mesh = mesh + # type: (compas.datastructures.Mesh) -> None + self._item = mesh self._transformation = None self._vertex_xyz = None @property def transformation(self): + # type: () -> compas.geometry.Transformation | None return self._transformation @transformation.setter def transformation(self, transformation): + # type: (compas.geometry.Transformation) -> None self._vertex_xyz = None self._transformation = transformation @property def vertex_xyz(self): - if self._vertex_xyz is None: - points = self.mesh.vertices_attributes("xyz") # type: ignore - points = transform_points(points, self.worldtransformation) - self._vertex_xyz = dict(zip(self.mesh.vertices(), points)) # type: ignore - return self._vertex_xyz + # type: () -> dict[int, list[float]] + if self.mesh: + if self._vertex_xyz is None: + points = self.mesh.vertices_attributes("xyz") + points = transform_points(points, self.worldtransformation) + self._vertex_xyz = dict(zip(self.mesh.vertices(), points)) + return self._vertex_xyz # type: ignore @vertex_xyz.setter def vertex_xyz(self, vertex_xyz): + # type: (dict[int, list[float]]) -> None self._vertex_xyz = vertex_xyz - def draw_vertices(self, vertices=None, color=None, text=None): + def draw_vertices(self): """Draw the vertices of the mesh. - Parameters - ---------- - vertices : list[int], optional - The vertices to include in the drawing. - Default is all vertices. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[int, tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the vertices, - as either a single color to be applied to all vertices, - or a color dict, mapping specific vertices to specific colors. - text : dict[int, str], optional - The text labels for the vertices - as a text dict, mapping specific vertices to specific text labels. + Vertices are drawn based on the values of + + * `self.show_vertices` + * `self.vertexcolor` + * `self.vertextext` + * `self.vertexsize` Returns ------- @@ -125,21 +153,15 @@ def draw_vertices(self, vertices=None, color=None, text=None): """ raise NotImplementedError - def draw_edges(self, edges=None, color=None, text=None): + def draw_edges(self): """Draw the edges of the mesh. - Parameters - ---------- - edges : list[tuple[int, int]], optional - The edges to include in the drawing. - Default is all edges. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[tuple[int, int], tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the edges, - as either a single color to be applied to all edges, - or a color dict, mapping specific edges to specific colors. - text : dict[tuple[int, int], str], optional - The text labels for the edges - as a text dict, mapping specific edges to specific text labels. + Edges are drawn based on the values of + + * `self.show_edges` + * `self.edgecolor` + * `self.edgetext` + * `self.edgewidth` Returns ------- @@ -149,21 +171,14 @@ def draw_edges(self, edges=None, color=None, text=None): """ raise NotImplementedError - def draw_faces(self, faces=None, color=None, text=None): + def draw_faces(self): """Draw the faces of the mesh. - Parameters - ---------- - faces : list[int], optional - The faces to include in the drawing. - Default is all faces. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[int, tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the faces, - as either a single color to be applied to all faces, - or a color dict, mapping specific faces to specific colors. - text : dict[int, str], optional - The text labels for the faces - as a text dict, mapping specific faces to specific text labels. + Faces are drawn based on the values of + + * `self.show_faces` + * `self.facecolor` + * `self.facetext` Returns ------- @@ -187,26 +202,6 @@ def draw_mesh(self, *args, **kwargs): """ return self.draw(*args, **kwargs) - def draw(self): - """draw the mesh. - - Returns - ------- - None - - """ - raise NotImplementedError - - def clear(self): - """Clear all components of the mesh. - - Returns - ------- - None - - """ - raise NotImplementedError - def clear_vertices(self): """Clear the vertices of the mesh. diff --git a/src/compas/scene/planeobject.py b/src/compas/scene/planeobject.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/compas/scene/scene.py b/src/compas/scene/scene.py index b181691740d..472aa70ecad 100644 --- a/src/compas/scene/scene.py +++ b/src/compas/scene/scene.py @@ -66,7 +66,7 @@ def add(node, parent, items): return scene def __init__(self, name="Scene", context=None): - # type: (str | "Scene", str | None) -> None + # type: (str, str | None) -> None super(Scene, self).__init__(name=name) super(Scene, self).add(TreeNode(name="ROOT")) self.context = context or detect_current_context() @@ -74,8 +74,6 @@ def __init__(self, name="Scene", context=None): @property def objects(self): # type: () -> list[SceneObject] - # this is flagged by the type checker - # because the tree returns nodes of type TreeNode return [node for node in self.nodes if not node.is_root] # type: ignore def add(self, item, parent=None, **kwargs): @@ -106,18 +104,20 @@ def add(self, item, parent=None, **kwargs): if kwargs["context"] != self.context: raise Exception("Object context should be the same as scene context: {} != {}".format(kwargs["context"], self.context)) del kwargs["context"] # otherwist the SceneObject receives "context" twice, which results in an error - sceneobject = SceneObject(item, context=self.context, **kwargs) # type: ignore + sceneobject = SceneObject(item=item, context=self.context, **kwargs) # type: ignore super(Scene, self).add(sceneobject, parent=parent) return sceneobject def clear(self): # type: () -> None - """Clear the current context of the scene.""" + """Clear everything from the current context of the scene.""" + clear() def clear_objects(self): # type: () -> None """Clear all objects inside the scene.""" + guids = [] for sceneobject in self.objects: guids += sceneobject.guids @@ -127,11 +127,11 @@ def clear_objects(self): def draw(self): """Draw the scene.""" - before_draw() - if not self.context: raise ValueError("No context detected.") + before_draw() + self.clear_objects() drawn_objects = [] diff --git a/src/compas/scene/sceneobject.py b/src/compas/scene/sceneobject.py index a3ea109818b..54d0ea823eb 100644 --- a/src/compas/scene/sceneobject.py +++ b/src/compas/scene/sceneobject.py @@ -2,14 +2,16 @@ from __future__ import division from __future__ import print_function -from abc import abstractmethod from functools import reduce from operator import mul import compas.colors # noqa: F401 +import compas.data # noqa: F401 import compas.datastructures # noqa: F401 import compas.geometry # noqa: F401 +import compas.scene # noqa: F401 from compas.colors import Color +from compas.data import Data from compas.datastructures import TreeNode from compas.geometry import Transformation @@ -82,12 +84,26 @@ class SceneObject(TreeNode): color = ColorAttribute() - def __new__(cls, item, **kwargs): + def __new__(cls, item=None, **kwargs): sceneobject_cls = get_sceneobject_cls(item, **kwargs) return super(SceneObject, cls).__new__(sceneobject_cls) - def __init__(self, item, name=None, color=None, opacity=1.0, show=True, frame=None, transformation=None, context=None, **kwargs): # fmt: skip - # type: (compas.geometry.Geometry | compas.datastructures.Datastructure, str | None, compas.colors.Color | None, float, bool, compas.geometry.Frame | None, compas.geometry.Transformation | None, str | None, dict) -> None + def __init__( + self, + item=None, # type: compas.data.Data | None + name=None, # type: str | None + color=None, # type: compas.colors.Color | None + opacity=1.0, # type: float + show=True, # type: bool + frame=None, # type: compas.geometry.Frame | None + transformation=None, # type: compas.geometry.Transformation | None + context=None, # type: str | None + **kwargs # type: dict + ): # fmt: skip + # type: (...) -> None + if not isinstance(item, Data): + raise ValueError("The item assigned to this scene object should be a data object: {}".format(type(item))) + name = name or item.name super(SceneObject, self).__init__(name=name, **kwargs) # the scene object needs to store the context @@ -124,12 +140,12 @@ def __repr__(self): @property def scene(self): - # type: () -> compas.scene.Scene + # type: () -> compas.scene.Scene | None return self.tree @property def item(self): - # type: () -> compas.geometry.Geometry | compas.datastructures.Datastructure + # type: () -> compas.data.Data return self._item @property @@ -179,12 +195,13 @@ def worldtransformation(self): @property def contrastcolor(self): - # type: () -> compas.colors.Color + # type: () -> compas.colors.Color | None if not self._contrastcolor: - if self.color.is_light: - self._contrastcolor = self.color.darkened(50) - else: - self._contrastcolor = self.color.lightened(50) + if self.color: + if self.color.is_light: + self._contrastcolor = self.color.darkened(50) + else: + self._contrastcolor = self.color.lightened(50) return self._contrastcolor @contrastcolor.setter @@ -195,7 +212,6 @@ def contrastcolor(self, color): @property def settings(self): # type: () -> dict - # The settings are all the nessessary attributes to reconstruct the scene object besides the Data item. settings = { "name": self.name, "color": self.color, @@ -211,6 +227,7 @@ def settings(self): return settings def add(self, item, **kwargs): + # type: (compas.data.Data, dict) -> SceneObject """Add a child item to the scene object. Parameters @@ -237,12 +254,11 @@ def add(self, item, **kwargs): if kwargs["context"] != self.context: raise Exception("Child context should be the same as parent context: {} != {}".format(kwargs["context"], self.context)) del kwargs["context"] # otherwist the SceneObject receives "context" twice, which results in an error - sceneobject = SceneObject(item, context=self.context, **kwargs) # type: ignore + sceneobject = SceneObject(item=item, context=self.context, **kwargs) # type: ignore super(SceneObject, self).add(sceneobject) return sceneobject - @abstractmethod def draw(self): """The main drawing method.""" raise NotImplementedError diff --git a/src/compas/scene/shapeobject.py b/src/compas/scene/shapeobject.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/compas/scene/surfaceobject.py b/src/compas/scene/surfaceobject.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/compas/scene/volmeshobject.py b/src/compas/scene/volmeshobject.py index 6ca1135b651..5b391cb4188 100644 --- a/src/compas/scene/volmeshobject.py +++ b/src/compas/scene/volmeshobject.py @@ -2,6 +2,9 @@ from __future__ import division from __future__ import print_function +import compas.colors # noqa: F401 +import compas.datastructures # noqa: F401 +import compas.geometry # noqa: F401 from compas.geometry import transform_points from .descriptors.colordict import ColorDictAttribute @@ -64,68 +67,93 @@ class VolMeshObject(SceneObject): facecolor = ColorDictAttribute() cellcolor = ColorDictAttribute() - def __init__(self, volmesh, **kwargs): - super(VolMeshObject, self).__init__(item=volmesh, **kwargs) - self._volmesh = None + def __init__( + self, + show_vertices=False, # type: bool | list + show_edges=True, # type: bool | list + show_faces=False, # type: bool | list + show_cells=True, # type: bool | list + vertexcolor=None, # type: compas.colors.Color | dict | None + edgecolor=None, # type: compas.colors.Color | dict | None + facecolor=None, # type: compas.colors.Color | dict | None + cellcolor=None, # type: compas.colors.Color | dict | None + vertexsize=1.0, # type: float + edgewidth=1.0, # type: float + **kwargs # type: dict + ): # fmt: skip + # type: (...) -> None + super(VolMeshObject, self).__init__(**kwargs) # type: ignore self._vertex_xyz = None - self.volmesh = volmesh - self.vertexcolor = kwargs.get("vertexcolor", self.color) - self.edgecolor = kwargs.get("edgecolor", self.color) - self.facecolor = kwargs.get("facecolor", self.color) - self.cellcolor = kwargs.get("cellcolor", self.color) - self.vertexsize = kwargs.get("vertexsize", 1.0) - self.edgewidth = kwargs.get("edgewidth", 1.0) - self.show_vertices = kwargs.get("show_vertices", False) - self.show_edges = kwargs.get("show_edges", True) - self.show_faces = kwargs.get("show_faces", False) - self.show_cells = kwargs.get("show_cells", True) + self.show_vertices = show_vertices + self.show_edges = show_edges + self.show_faces = show_faces + self.show_cells = show_cells + self.vertexcolor = vertexcolor or self.color + self.edgecolor = edgecolor or self.color + self.facecolor = facecolor or self.color + self.cellcolor = cellcolor or self.color + self.vertexsize = vertexsize + self.edgewidth = edgewidth + + @property + def settings(self): + # type: () -> dict + settings = super(VolMeshObject, self).settings + settings["show_vertices"] = self.show_vertices + settings["show_edges"] = self.show_edges + settings["show_faces"] = self.show_faces + settings["show_cells"] = self.show_cells + settings["vertexcolor"] = self.vertexcolor + settings["edgecolor"] = self.edgecolor + settings["facecolor"] = self.facecolor + settings["cellcolor"] = self.cellcolor + return settings @property def volmesh(self): - return self._volmesh + # type: () -> compas.datastructures.VolMesh + return self.item # type: ignore @volmesh.setter def volmesh(self, volmesh): - self._volmesh = volmesh + # type: (compas.datastructures.VolMesh) -> None + self._item = volmesh self._transformation = None self._vertex_xyz = None @property def transformation(self): + # type: () -> compas.geometry.Transformation | None return self._transformation @transformation.setter def transformation(self, transformation): + # type: (compas.geometry.Transformation) -> None self._vertex_xyz = None self._transformation = transformation @property def vertex_xyz(self): + # type: () -> dict[int, list[float]] if self._vertex_xyz is None: points = self.volmesh.vertices_attributes("xyz") # type: ignore points = transform_points(points, self.worldtransformation) self._vertex_xyz = dict(zip(self.volmesh.vertices(), points)) # type: ignore - return self._vertex_xyz + return self._vertex_xyz # type: ignore @vertex_xyz.setter def vertex_xyz(self, vertex_xyz): + # type: (dict[int, list[float]]) -> None self._vertex_xyz = vertex_xyz - def draw_vertices(self, vertices=None, color=None, text=None): + def draw_vertices(self): """Draw the vertices of the mesh. - Parameters - ---------- - vertices : list[int], optional - The vertices to include in the drawing. - Default is all vertices. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[int, tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the vertices, - as either a single color to be applied to all vertices, - or a color dict, mapping specific vertices to specific colors. - text : dict[int, str], optional - The text labels for the vertices as a text dict, - mapping specific vertices to specific text labels. + The vertices are drawn based on the values of + + * `self.show_vertices` + * `self.vertexcolor` + * `self.vertexsize` Returns ------- @@ -135,21 +163,14 @@ def draw_vertices(self, vertices=None, color=None, text=None): """ raise NotImplementedError - def draw_edges(self, edges=None, color=None, text=None): + def draw_edges(self): """Draw the edges of the mesh. - Parameters - ---------- - edges : list[tuple[int, int]], optional - The edges to include in the drawing. - Default is all edges. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[tuple[int, int], tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the edges, - as either a single color to be applied to all edges, - or a color dict, mapping specific edges to specific colors. - text : dict[tuple[int, int], str], optional - The text labels for the edges as a text dict, - mapping specific edges to specific text labels. + The edges are drawn based on the values of + + * `self.show_edges` + * `self.edgecolor` + * `self.edgewidth` Returns ------- @@ -159,21 +180,13 @@ def draw_edges(self, edges=None, color=None, text=None): """ raise NotImplementedError - def draw_faces(self, faces=None, color=None, text=None): + def draw_faces(self): """Draw the faces of the mesh. - Parameters - ---------- - faces : list[int], optional - The faces to include in the drawing. - Default is all faces. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[int, tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the faces, - as either a single color to be applied to all faces, - or a color dict, mapping specific faces to specific colors. - text : dict[int, str], optional - The text labels for the faces as a text dict, - mapping specific faces to specific text labels. + The faces are drawn based on the values of + + * `self.show_faces` + * `self.facecolor` Returns ------- @@ -183,21 +196,13 @@ def draw_faces(self, faces=None, color=None, text=None): """ raise NotImplementedError - def draw_cells(self, cells=None, color=None, text=None): + def draw_cells(self): """Draw the cells of the mesh. - Parameters - ---------- - cells : list[int], optional - The cells to include in the drawing. - Default is all cells. - color : tuple[float, float, float] | :class:`compas.colors.Color` | dict[int, tuple[float, float, float] | :class:`compas.colors.Color`], optional - The color of the cells, - as either a single color to be applied to all cells, - or a color dict, mapping specific cells to specific colors. - text : dict[int, str], optional - The text labels for the cells as a text dict, - mapping specific cells to specific text labels. + The cells are drawn based on the values of + + * `self.show_cells` + * `self.cellcolor` Returns ------- @@ -207,10 +212,6 @@ def draw_cells(self, cells=None, color=None, text=None): """ raise NotImplementedError - def draw(self): - """Draw the volmesh.""" - raise NotImplementedError - def clear_vertices(self): """Clear the vertices of the mesh. diff --git a/src/compas_blender/utilities/drawing.py b/src/compas_blender/drawing.py similarity index 100% rename from src/compas_blender/utilities/drawing.py rename to src/compas_blender/drawing.py diff --git a/src/compas_blender/scene/__init__.py b/src/compas_blender/scene/__init__.py index 414cef7dfea..021a7716939 100644 --- a/src/compas_blender/scene/__init__.py +++ b/src/compas_blender/scene/__init__.py @@ -31,12 +31,10 @@ from compas.datastructures import VolMesh from .sceneobject import BlenderSceneObject +from .shapeobject import ShapeObject from .boxobject import BoxObject -from .capsuleobject import CapsuleObject from .circleobject import CircleObject -from .coneobject import ConeObject from .curveobject import CurveObject -from .cylinderobject import CylinderObject from .frameobject import FrameObject from .lineobject import LineObject from .meshobject import MeshObject @@ -47,9 +45,7 @@ from .polygonobject import PolygonObject from .polyhedronobject import PolyhedronObject from .polylineobject import PolylineObject -from .sphereobject import SphereObject from .surfaceobject import SurfaceObject -from .torusobject import TorusObject from .vectorobject import VectorObject from .volmeshobject import VolMeshObject @@ -67,11 +63,11 @@ def after_draw_blender(drawn_objects): @plugin(category="factories", requires=["bpy"]) def register_scene_objects(): register(Box, BoxObject, context="Blender") - register(Capsule, CapsuleObject, context="Blender") + register(Capsule, ShapeObject, context="Blender") register(Circle, CircleObject, context="Blender") - register(Cone, ConeObject, context="Blender") + register(Cone, ShapeObject, context="Blender") register(Curve, CurveObject, context="Blender") - register(Cylinder, CylinderObject, context="Blender") + register(Cylinder, ShapeObject, context="Blender") register(Frame, FrameObject, context="Blender") register(Line, LineObject, context="Blender") register(Mesh, MeshObject, context="Blender") @@ -82,9 +78,9 @@ def register_scene_objects(): register(Polygon, PolygonObject, context="Blender") register(Polyhedron, PolyhedronObject, context="Blender") register(Polyline, PolylineObject, context="Blender") - register(Sphere, SphereObject, context="Blender") + register(Sphere, ShapeObject, context="Blender") register(Surface, SurfaceObject, context="Blender") - register(Torus, TorusObject, context="Blender") + register(Torus, ShapeObject, context="Blender") register(Vector, VectorObject, context="Blender") register(VolMesh, VolMeshObject, context="Blender") print("Blender Objects registered.") @@ -92,10 +88,9 @@ def register_scene_objects(): __all__ = [ "BlenderSceneObject", + "shapeObject", "BoxObject", - "CapsuleObject", "CircleObject", - "ConeObject", "CurveObject", "CylinderObject", "FrameObject", diff --git a/src/compas_blender/scene/boxobject.py b/src/compas_blender/scene/boxobject.py index 05f0da9b9fd..7da0b47379f 100644 --- a/src/compas_blender/scene/boxobject.py +++ b/src/compas_blender/scene/boxobject.py @@ -1,11 +1,5 @@ -from typing import Any -from typing import Optional -from typing import Union - import bpy # type: ignore -from compas.colors import Color -from compas.geometry import Box from compas.scene import GeometryObject from compas_blender import conversions @@ -13,52 +7,21 @@ class BoxObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing box shapes in Blender. - - Parameters - ---------- - box : :class:`compas.geometry.Box` - A COMPAS box. - **kwargs : dict, optional - Additional keyword arguments. - - """ + """Scene object for drawing box shapes in Blender.""" - def __init__(self, box: Box, **kwargs: Any): - super().__init__(geometry=box, **kwargs) - - def draw( - self, - color: Optional[Color] = None, - collection: Optional[Union[str, bpy.types.Collection]] = None, - show_wire: bool = True, - ) -> list[bpy.types.Object]: + def draw(self) -> list[bpy.types.Object]: """Draw the box associated with the scene object. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`compas.colors.Color`, optional - The RGB color of the box. - 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`] The object(s) created in Blender to represent the box. """ - color = Color.coerce(color) or self.color - 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) + mesh = conversions.vertices_and_faces_to_blender_mesh(self.geometry.vertices, self.geometry.faces, name=self.geometry.name) - obj = self.create_object(mesh, name=name) - self.update_object(obj, color=color, collection=collection, show_wire=show_wire) + obj = self.create_object(mesh, name=self.geometry.name) + self.update_object(obj, color=self.color, collection=self.collection, show_wire=self.show_wire) self._guids = [obj] return self.guids diff --git a/src/compas_blender/scene/capsuleobject.py b/src/compas_blender/scene/capsuleobject.py deleted file mode 100644 index da8f157d93e..00000000000 --- a/src/compas_blender/scene/capsuleobject.py +++ /dev/null @@ -1,73 +0,0 @@ -from typing import Any -from typing import Optional - -import bpy # type: ignore - -from compas.colors import Color -from compas.geometry import Capsule -from compas.scene import GeometryObject -from compas_blender import conversions - -from .sceneobject import BlenderSceneObject - - -class CapsuleObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing capsule shapes in Blender. - - Parameters - ---------- - capsule : :class:`compas.geometry.Capsule` - A COMPAS capsule. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - 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, - ) -> list[bpy.types.Object]: - """Draw the capsule associated with the scene object. - - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`compas.colors.Color`, optional - The RGB color of the capsule. - 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. - v : int, optional - Number of faces in the "v" direction. - 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`] - The objects created in Blender. - - """ - name = self.geometry.name - color = Color.coerce(color) or self.color - - 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) - - self._guids = [obj] - return self.guids diff --git a/src/compas_blender/scene/circleobject.py b/src/compas_blender/scene/circleobject.py index 3a5a15b18a0..f45b9251f61 100644 --- a/src/compas_blender/scene/circleobject.py +++ b/src/compas_blender/scene/circleobject.py @@ -1,29 +1,15 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Circle from compas.scene import GeometryObject from .sceneobject import BlenderSceneObject class CircleObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing circles in Blender. - - Parameters - ---------- - circle : :class:`compas.geometry.Circle` - A COMPAS circle. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, circle: Circle, **kwargs: Any): - super().__init__(geometry=circle, **kwargs) + """Scene object for drawing circles in Blender.""" def draw(self, color: Optional[Color] = None, collection: Optional[str] = None) -> list[bpy.types.Object]: """Draw the circle. @@ -47,7 +33,7 @@ def draw(self, color: Optional[Color] = None, collection: Optional[str] = None) obj = bpy.context.object self.objects.append(obj) - self.update_object(obj, color=color, collection=collection, transformation=self.geometry.transformation) + self.update_object(obj, color=color, collection=collection) self._guids = [obj] return self.guids diff --git a/src/compas_blender/scene/coneobject.py b/src/compas_blender/scene/coneobject.py deleted file mode 100644 index b7b6ea7aeca..00000000000 --- a/src/compas_blender/scene/coneobject.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Any -from typing import Optional - -import bpy # type: ignore - -from compas.colors import Color -from compas.geometry import Cone -from compas.scene import GeometryObject -from compas_blender import conversions - -from .sceneobject import BlenderSceneObject - - -class ConeObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing cone shapes in Blender. - - Parameters - ---------- - cone : :class:`compas.geometry.Cone` - A COMPAS cone. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - 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 = False, - ) -> list[bpy.types.Object]: - """Draw the cone associated with the scene object. - - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`compas.colors.Color`, optional - The RGB color of the cone. - 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. - 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`] - The objects created in Blender. - - """ - name = self.geometry.name - color = Color.coerce(color) or self.color - - 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) - - self._guids = [obj] - return self.guids diff --git a/src/compas_blender/scene/curveobject.py b/src/compas_blender/scene/curveobject.py index 39244369711..96e7617db0a 100644 --- a/src/compas_blender/scene/curveobject.py +++ b/src/compas_blender/scene/curveobject.py @@ -1,10 +1,8 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Curve from compas.scene import GeometryObject from compas_blender import conversions @@ -12,19 +10,7 @@ class CurveObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing curves in Blender. - - Parameters - ---------- - curve : :class:`compas.geometry.Curve` - A COMPAS curve. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, curve: Curve, **kwargs: Any): - super().__init__(geometry=curve, **kwargs) + """Scene object for drawing curves in Blender.""" def draw( self, diff --git a/src/compas_blender/scene/cylinderobject.py b/src/compas_blender/scene/cylinderobject.py deleted file mode 100644 index ae7e198d3a9..00000000000 --- a/src/compas_blender/scene/cylinderobject.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Any -from typing import Optional - -import bpy # type: ignore - -from compas.colors import Color -from compas.geometry import Cylinder -from compas.scene import GeometryObject -from compas_blender import conversions - -from .sceneobject import BlenderSceneObject - - -class CylinderObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing cylinder shapes in Blender. - - Parameters - ---------- - cylinder : :class:`compas.geometry.Cylinder` - A COMPAS cylinder. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - 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 = False, - ) -> list[bpy.types.Object]: - """Draw the cylinder associated with the scene object. - - 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. - 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`] - The objects created in Blender. - - """ - name = self.geometry.name - color = Color.coerce(color) or self.color - - 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) - - self._guids = [obj] - return self.guids diff --git a/src/compas_blender/scene/frameobject.py b/src/compas_blender/scene/frameobject.py index 8c58e5d1fbb..7a4a7e65d19 100644 --- a/src/compas_blender/scene/frameobject.py +++ b/src/compas_blender/scene/frameobject.py @@ -4,7 +4,6 @@ import bpy # type: ignore from compas.colors import Color -from compas.geometry import Frame from compas.geometry import Line from compas.scene import GeometryObject from compas_blender import conversions @@ -17,8 +16,6 @@ class FrameObject(BlenderSceneObject, GeometryObject): Parameters ---------- - frame: :class:`compas.geometry.Frame` - A COMPAS frame. scale : float, optional Scale of the frame axes. **kwargs : dict, optional @@ -40,8 +37,8 @@ class FrameObject(BlenderSceneObject, GeometryObject): """ - def __init__(self, frame: Frame, scale=1.0, **kwargs: Any): - super().__init__(geometry=frame, **kwargs) + def __init__(self, scale=1.0, **kwargs: Any): + super().__init__(**kwargs) self.scale = scale self.color_origin = Color.black() self.color_xaxis = Color.red() diff --git a/src/compas_blender/scene/graphobject.py b/src/compas_blender/scene/graphobject.py index f33a4145ab5..551c33e0041 100644 --- a/src/compas_blender/scene/graphobject.py +++ b/src/compas_blender/scene/graphobject.py @@ -1,36 +1,52 @@ -from typing import Any -from typing import Dict from typing import List -from typing import Optional -from typing import Tuple -from typing import Union import bpy # type: ignore import compas_blender -from compas.colors import Color -from compas.datastructures import Graph +import compas_blender.objects from compas.geometry import Line -from compas.scene import GraphObject as BaseSceneObject +from compas.geometry import Sphere +from compas.scene import GraphObject as BaseGraphObject from compas_blender import conversions from .sceneobject import BlenderSceneObject -class GraphObject(BlenderSceneObject, BaseSceneObject): +class GraphObject(BlenderSceneObject, BaseGraphObject): """Scene object for drawing graph data structures in Blender. Parameters ---------- - graph : :class:`compas.datastructures.Graph` - A COMPAS graph. + node_u : int, optional + Number of segments in the U direction of the node spheres. + Default is ``16``. + node_v : int, optional + Number of segments in the V direction of the node spheres. + Default is ``16``. + **kwargs : dict, optional + Additional keyword arguments. + For more info, + see :class:`compas_blender.scene.BlenderSceneObject` and :class:`compas.scene.GraphObject`. + + Attributes + ---------- + node_u : int + Number of segments in the U direction of the node spheres. + node_v : int + Number of segments in the V direction of the node spheres. + nodeobjects : list[:blender:`bpy.types.Object`] + List of Blender objects representing the nodes. + edgeobjects : list[:blender:`bpy.types.Object`] + List of Blender objects representing the edges. """ - def __init__(self, graph: Graph, **kwargs: Any): - super().__init__(graph=graph, **kwargs) + def __init__(self, node_u=16, node_v=16, **kwargs: dict): + super().__init__(**kwargs) self.nodeobjects = [] self.edgeobjects = [] + self.node_u = node_u + self.node_v = node_v # ========================================================================== # clear @@ -44,7 +60,7 @@ def clear_nodes(self): None """ - compas_blender.delete_objects(self.nodeobjects) + compas_blender.objects.delete_objects(self.nodeobjects) def clear_edges(self): """Clear all objects contained in the edge collection. @@ -54,84 +70,27 @@ def clear_edges(self): None """ - compas_blender.delete_objects(self.edgeobjects) - - # def clear_nodelabels(self): - # """Clear all objects contained in the nodelabel collection. - - # Returns - # ------- - # None - - # """ - # compas_blender.delete_objects(self.nodelabelcollection.objects) - - # def clear_edgelabels(self): - # """Clear all objects contained in the edgelabel collection. - - # Returns - # ------- - # None - - # """ - # compas_blender.delete_objects(self.edgelabelcollection.objects) + compas_blender.objects.delete_objects(self.edgeobjects) # ========================================================================== # draw # ========================================================================== - def draw( - self, - nodes: Optional[List[int]] = None, - edges: Optional[Tuple[int, int]] = None, - nodecolor: Optional[Union[Color, Dict[int, Color]]] = None, - edgecolor: Optional[Union[Color, Dict[Tuple[int, int], Color]]] = None, - ) -> list[bpy.types.Object]: + def draw(self) -> list[bpy.types.Object]: """Draw the graph. - Parameters - ---------- - 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. - Returns ------- list[:blender:`bpy.types.Object`] The objects created in Blender. """ - self._guids = self.draw_nodes(nodes=nodes, color=nodecolor) + self.draw_edges(edges=edges, color=edgecolor) + self._guids = self.draw_nodes() + self.draw_edges() return self.guids - 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]: + def draw_nodes(self) -> List[bpy.types.Object]: """Draw a selection of nodes. - Parameters - ---------- - nodes : list[hashable], optional - A list of node identifiers. - 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. - collection : str, optional - The name of the Blender scene collection containing the created object(s). - Returns ------- list[:blender:`bpy.types.Object`] @@ -139,40 +98,33 @@ def draw_nodes( """ objects = [] - self.nodecolor = color + nodes = list(self.graph.nodes()) if self.show_nodes is True else self.show_nodes or [] + + for node in nodes: + name = f"{self.graph.name}.node.{node}" + color = self.nodecolor[node] + point = self.graph.node_coordinates(node) + + # # there is no such thing as a sphere data block + # # this doesn't work with application of worl transformation matrix + # bpy.ops.mesh.primitive_uv_sphere_add(location=point, radius=self.nodesize, segments=self.node_u, ring_count=self.node_v) + # obj = bpy.context.object + # self.update_object(obj, name=name, color=color, collection=self.collection) + sphere = Sphere(radius=self.nodesize, point=point) + sphere.resolution_u = self.node_u + sphere.resolution_v = self.node_v + mesh = conversions.vertices_and_faces_to_blender_mesh(sphere.vertices, sphere.faces, name=name) + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=self.collection) - for node in nodes or self.graph.nodes(): # type: ignore - name = f"{self.graph.name}.node.{node}" # type: ignore - color = self.nodecolor[node] # type: ignore - point = self.graph.nodes_attributes("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) + self.nodeobjects = objects return objects - def draw_edges( - self, - edges: Optional[Tuple[int, int]] = None, - color: Optional[Union[Color, Dict[Tuple[int, int], Color]]] = None, - collection: Optional[str] = None, - ) -> List[bpy.types.Object]: + def draw_edges(self) -> List[bpy.types.Object]: """Draw a selection of edges. - Parameters - ---------- - 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. - color : :class:`compas.colors.Color` | dict[tuple[hashable, hashable], :class:`compas.colors.Color`], optional - The color specification for the edges. - collection : str, optional - The name of the Blender scene collection containing the created object(s). - Returns ------- list[:blender:`bpy.types.Object`] @@ -180,78 +132,16 @@ def draw_edges( """ objects = [] - self.edgecolor = color + edges = list(self.graph.edges()) if self.show_edges is True else self.show_edges or [] - for u, v in edges or self.graph.edges(): # type: ignore - name = f"{self.graph.name}.edge.{u}-{v}" # type: ignore - color = self.edgecolor[u, v] # type: ignore - curve = conversions.line_to_blender_curve(Line(self.graph.nodes_attributes("xyz")[u], self.graph.nodes_attributes("xyz")[v])) + for u, v in edges: + name = f"{self.graph.name}.edge.{u}-{v}" + color = self.edgecolor[u, v] + curve = conversions.line_to_blender_curve(Line(self.graph.node_coordinates(u), self.graph.node_coordinates(v))) obj = self.create_object(curve, name=name) - self.update_object(obj, color=color, collection=collection) + self.update_object(obj, color=color, collection=self.collection) objects.append(obj) + self.edgeobjects = objects 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.graph.nodes_attributes("xyz")[node], - # "name": f"{self.graph.name}.nodelabel.{node}", - # "text": self.node_text[node], - # "color": self.nodecolor[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.graph.nodes_attributes("xyz")[u], self.graph.nodes_attributes("xyz")[v]]), - # "name": f"{self.graph.name}.edgelabel.{u}-{v}", - # "text": self.edge_text[edge], - # "color": self.edgecolor[edge], - # } - # ) - # return compas_blender.draw_texts(labels, collection=self.edgelabelcollection) - - # ============================================================================= - # draw miscellaneous - # ============================================================================= diff --git a/src/compas_blender/scene/lineobject.py b/src/compas_blender/scene/lineobject.py index 88cd3c96b9f..b49d29996cf 100644 --- a/src/compas_blender/scene/lineobject.py +++ b/src/compas_blender/scene/lineobject.py @@ -1,10 +1,8 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Line from compas.scene import GeometryObject from compas_blender import conversions @@ -12,21 +10,7 @@ class LineObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing lines in Blender. - - Parameters - ---------- - line : :class:`compas.geometry.Line` - A COMPAS line. - **kwargs : dict, optional - Additional keyword arguments. - For more info, - see :class:`compas_blender.scene.BlenderSceneObject` and :class:`compas.scene.GeometryObject`. - - """ - - def __init__(self, line: Line, **kwargs: Any): - super().__init__(geometry=line, **kwargs) + """Scene object for drawing lines in Blender.""" def draw( self, diff --git a/src/compas_blender/scene/meshobject.py b/src/compas_blender/scene/meshobject.py index 22ba36beebd..315201ac2a0 100644 --- a/src/compas_blender/scene/meshobject.py +++ b/src/compas_blender/scene/meshobject.py @@ -1,21 +1,12 @@ from typing import Any -from typing import Dict from typing import List -from typing import Optional -from typing import Tuple -from typing import Union import bpy # type: ignore import compas_blender -from compas.colors import Color -from compas.datastructures import Mesh -from compas.geometry import Cylinder +import compas_blender.objects from compas.geometry import Line from compas.geometry import Sphere -from compas.geometry import add_vectors -from compas.geometry import centroid_points -from compas.geometry import scale_vector from compas.scene import MeshObject as BaseMeshObject from compas_blender import conversions @@ -27,23 +18,46 @@ class MeshObject(BlenderSceneObject, BaseMeshObject): Parameters ---------- - mesh : :class:`compas.datastructures.Mesh` - A COMPAS mesh. + vertex_u : int, optional + Number of segments in the U direction of the vertex spheres. + Default is ``16``. + vertex_v : int, optional + Number of segments in the V direction of the vertex spheres. + Default is ``16``. + **kwargs : dict, optional + Additional keyword arguments. + For more info, + see :class:`compas_blender.scene.BlenderSceneObject` and :class:`compas.scene.MeshObject`. + + Attributes + ---------- + vertex_u : int + Number of segments in the U direction of the vertex spheres. + vertex_v : int + Number of segments in the V direction of the vertex spheres. + vertexobjects : list[:blender:`bpy.types.Object`] + List of Blender objects representing the vertices. + edgeobjects : list[:blender:`bpy.types.Object`] + List of Blender objects representing the edges. + faceobjects : list[:blender:`bpy.types.Object`] + List of Blender objects representing the faces. """ - def __init__(self, mesh: Mesh, **kwargs: Any): - super().__init__(mesh=mesh, **kwargs) + def __init__(self, vertex_u=16, vertex_v=16, **kwargs: Any): + super().__init__(**kwargs) self.vertexobjects = [] self.edgeobjects = [] self.faceobjects = [] + self.vertex_u = vertex_u + self.vertex_v = vertex_v # ========================================================================== # clear # ========================================================================== def clear(self): - compas_blender.delete_objects(self.objects) + compas_blender.objects.delete_objects(self.objects) def clear_vertices(self): """Clear the vertex objects. @@ -53,7 +67,7 @@ def clear_vertices(self): None """ - compas_blender.delete_objects(self.vertexobjects) + compas_blender.objects.delete_objects(self.vertexobjects) def clear_edges(self): """Clear the edge objects. @@ -63,7 +77,7 @@ def clear_edges(self): None """ - compas_blender.delete_objects(self.edgeobjects) + compas_blender.objects.delete_objects(self.edgeobjects) def clear_faces(self): """Clear the face objects. @@ -73,110 +87,46 @@ def clear_faces(self): None """ - compas_blender.delete_objects(self.faceobjects) + compas_blender.objects.delete_objects(self.faceobjects) # ========================================================================== # draw # ========================================================================== - def draw(self, color: Optional[Color] = None, collection: Optional[str] = None, show_wire: bool = True) -> list[bpy.types.Object]: + def draw(self) -> list[bpy.types.Object]: """Draw the mesh. - Parameters - ---------- - color : :class:`compas.colors.Color`, optional - The color of the mesh. - 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`] The objects created in Blender. """ - 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) + self._guids = [] - self._guids = [obj] - return self.guids - - 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. - - Parameters - ---------- - vertices : list[int], optional - A list of vertex keys identifying which 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. - 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`] + if self.show_faces is True: + name = self.mesh.name # type: ignore + color = self.color + mesh = conversions.mesh_to_blender(self.mesh) # type: ignore - """ - objects = [] + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=self.collection, show_wire=self.show_wire) - self.vertexcolor = color + self._guids.append(obj) - for vertex in vertices or self.mesh.vertices(): # type: ignore - name = f"{self.mesh.name}.vertex.{vertex}" # type: ignore - color = self.vertexcolor[vertex] # type: ignore - point = self.vertex_xyz[vertex] + elif self.show_faces: + self._guids += self.draw_faces() - # 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) + if self.show_vertices: + self._guids += self.draw_vertices() - return objects + if self.show_edges: + self._guids += self.draw_edges() - def draw_edges( - self, - edges: Optional[List[Tuple[int, int]]] = None, - color: Optional[Union[Color, Dict[Tuple[int, int], Color]]] = None, - collection: Optional[str] = None, - ) -> List[bpy.types.Object]: - """Draw a selection of edges. + return self.guids - 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 : :class:`compas.colors.Color` | dict[tuple[int, int], :class:`compas.colors.Color`], optional - The color specification for the edges. - collection : str, optional - The name of the Blender scene collection containing the created object(s). + def draw_vertices(self) -> List[bpy.types.Object]: + """Draw a selection of vertices. Returns ------- @@ -185,85 +135,31 @@ def draw_edges( """ objects = [] - self.edgecolor = color + vertices = list(self.mesh.vertices()) if self.show_vertices is True else self.show_vertices or [] - for u, v in edges or self.mesh.edges(): # type: ignore - name = f"{self.mesh.name}.edge.{u}-{v}" # type: ignore - color = self.edgecolor[u, v] # type: ignore - curve = conversions.line_to_blender_curve(Line(self.vertex_xyz[u], self.vertex_xyz[v])) + for vertex in vertices: + name = f"{self.mesh.name}.vertex.{vertex}" + color = self.vertexcolor[vertex] + point = self.mesh.vertex_coordinates(vertex) - 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. - - Parameters - ---------- - faces : list[int], optional - A list of face keys identifying which 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. - 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.facecolor = color - - for face in faces or self.mesh.faces(): # type: ignore - name = f"{self.mesh.name}.face.{face}" # type: ignore - color = self.facecolor[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 + # # 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.update_object(obj, name=name, color=color, collection=collection) # type: ignore + sphere = Sphere(radius=self.vertexsize, point=point) + sphere.resolution_u = self.vertex_u + sphere.resolution_v = self.vertex_v + mesh = conversions.vertices_and_faces_to_blender_mesh(sphere.vertices, sphere.faces, name=name) obj = self.create_object(mesh, name=name) - self.update_object(obj, color=color, collection=collection, show_wire=show_wire) # type: ignore + self.update_object(obj, color=color, collection=self.collection) objects.append(obj) + self.vertexobjects = objects return objects - # ========================================================================== - # draw normals - # ========================================================================== - - def draw_vertexnormals( - self, - 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. - - Parameters - ---------- - vertices : list[int], optional - A selection of vertex normals to draw. - Default is to draw all vertex normals. - color : :class:`compas.colors.Color`, optional - 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). + def draw_edges(self) -> List[bpy.types.Object]: + """Draw a selection of edges. Returns ------- @@ -272,44 +168,22 @@ def draw_vertexnormals( """ 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 + edges = list(self.mesh.edges()) if self.show_edges is True else self.show_edges or [] - a = self.vertex_xyz[vertex] - n = self.mesh.vertex_normal(vertex) # type: ignore - b = add_vectors(a, scale_vector(n, scale)) - - curve = conversions.line_to_blender_curve(Line(a, b)) + for u, v in edges: + name = f"{self.mesh.name}.edge.{u}-{v}" + color = self.edgecolor[u, v] + curve = conversions.line_to_blender_curve(Line(self.mesh.vertex_coordinates(u), self.mesh.vertex_coordinates(v))) obj = self.create_object(curve, name=name) - self.update_object(obj, color=color, collection=collection) - + self.update_object(obj, color=color, collection=self.collection) objects.append(obj) + self.edgeobjects = objects 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. - - Parameters - ---------- - faces : list[int], optional - A selection of face normals to draw. - Default is to draw all face normals. - color : :class:`compas.colors.Color`, optional - 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). + def draw_faces(self) -> List[bpy.types.Object]: + """Draw a selection of faces. Returns ------- @@ -318,191 +192,113 @@ def draw_facenormals( """ 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)) + faces = list(self.mesh.faces()) if self.show_faces is True else self.show_faces or [] - curve = conversions.line_to_blender_curve(Line(a, b)) - - obj = self.create_object(curve, name=name) - self.update_object(obj, color=color, collection=collection) + for face in faces: + name = f"{self.mesh.name}.face.{face}" + color = self.facecolor[face] + points = [self.mesh.vertex_coordinates(vertex) for vertex in self.mesh.face_vertices(face)] + mesh = conversions.polygon_to_blender_mesh(points, name=name) + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=self.collection) objects.append(obj) + self.faceobjects = objects return objects # ========================================================================== - # draw labels + # draw normals # ========================================================================== - # def draw_vertexlabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: - # """Draw labels for a selection vertices. + # def draw_vertexnormals( + # self, + # 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. # 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. + # vertices : list[int], optional + # A selection of vertex normals to draw. + # Default is to draw all vertex normals. + # color : :class:`compas.colors.Color`, optional + # 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`] # """ - # 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.vertexcolor[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. + # color = Color.coerce(color) # type: ignore - # Returns - # ------- - # list[:blender:`bpy.types.Object`] + # for vertex in vertices or self.mesh.vertices(): # type: ignore + # name = f"{self.mesh.name}.vertex.{vertex}.normal" # 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.edgecolor[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. + # a = self.vertex_xyz[vertex] + # n = self.mesh.vertex_normal(vertex) # type: ignore + # b = add_vectors(a, scale_vector(n, scale)) + + # 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. # 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. + # faces : list[int], optional + # A selection of face normals to draw. + # Default is to draw all face normals. + # color : :class:`compas.colors.Color`, optional + # 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`] # """ - # 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.facecolor[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 - ---------- - 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`] - - """ - objects = [] + # objects = [] - self.vertexcolor = color + # color = Color.coerce(color) # type: ignore - for vertex in radius: - name = "{}.vertex.{}.sphere".format(self.mesh.name, vertex) # type: ignore - color = self.vertexcolor[vertex] # type: ignore + # for face in faces or self.mesh.faces(): # type: ignore + # name = f"{self.mesh.name}.face.{face}.normal" # type: ignore - sphere = Sphere.from_point_and_radius(self.vertex_xyz[vertex], radius[vertex]) - geometry = conversions.sphere_to_blender_mesh(sphere, name=name) + # 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)) - obj = self.create_object(geometry, name=name) - self.update_object(obj, color=color, collection=collection) # type: ignore + # curve = conversions.line_to_blender_curve(Line(a, b)) - objects.append(obj) + # obj = self.create_object(curve, name=name) + # self.update_object(obj, color=color, collection=collection) - 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 - ---------- - 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`] + # objects.append(obj) - """ - objects = [] - - self.edgecolor = color - - for u, v in radius: - name = "{}.edge.{}-{}.pipe".format(self.mesh.name, u, v) # type: ignore - color = self.edgecolor[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 + # return objects diff --git a/src/compas_blender/scene/planeobject.py b/src/compas_blender/scene/planeobject.py index 320692d2f90..cbf3c5a27a1 100644 --- a/src/compas_blender/scene/planeobject.py +++ b/src/compas_blender/scene/planeobject.py @@ -6,7 +6,6 @@ from compas.colors import Color from compas.geometry import Frame from compas.geometry import Line -from compas.geometry import Plane from compas.scene import GeometryObject from compas_blender import conversions @@ -18,8 +17,6 @@ class PlaneObject(BlenderSceneObject, GeometryObject): Parameters ---------- - plane: :class:`compas.geometry.Plane` - A COMPAS plane. scale : float, optional Scale of the plane. **kwargs : dict, optional @@ -34,8 +31,8 @@ class PlaneObject(BlenderSceneObject, GeometryObject): """ - def __init__(self, plane: Plane, scale=1.0, **kwargs: Any): - super().__init__(geometry=plane, **kwargs) + def __init__(self, scale=1.0, **kwargs: Any): + super().__init__(**kwargs) self.scale = scale def draw(self, color: Optional[Color] = None, collection: Optional[str] = None) -> list[bpy.types.Object]: diff --git a/src/compas_blender/scene/pointcloudobject.py b/src/compas_blender/scene/pointcloudobject.py index 26093a3e9bb..4ffa7eebd14 100644 --- a/src/compas_blender/scene/pointcloudobject.py +++ b/src/compas_blender/scene/pointcloudobject.py @@ -1,10 +1,8 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Point from compas.scene import GeometryObject from compas_blender import conversions @@ -12,19 +10,7 @@ class PointcloudObject(BlenderSceneObject, GeometryObject): - """Scene object 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) + """Scene object for drawing pointclouds in Blender.""" def draw( self, diff --git a/src/compas_blender/scene/pointobject.py b/src/compas_blender/scene/pointobject.py index 3da3c63d083..1eee9eb6a89 100644 --- a/src/compas_blender/scene/pointobject.py +++ b/src/compas_blender/scene/pointobject.py @@ -1,29 +1,15 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Point from compas.scene import GeometryObject from .sceneobject import BlenderSceneObject class PointObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing points in Blender. - - Parameters - ---------- - point : :class:`compas.geometry.Point` - A COMPAS point. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, point: Point, **kwargs: Any): - super().__init__(geometry=point, **kwargs) + """Scene object for drawing points in Blender.""" def draw( self, diff --git a/src/compas_blender/scene/polygonobject.py b/src/compas_blender/scene/polygonobject.py index 2bbef35640d..7f9c694e88d 100644 --- a/src/compas_blender/scene/polygonobject.py +++ b/src/compas_blender/scene/polygonobject.py @@ -1,10 +1,8 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Polygon from compas.scene import GeometryObject from compas_blender import conversions @@ -12,19 +10,7 @@ class PolygonObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing polygons in Blender. - - Parameters - ---------- - polygon : :class:`compas.geometry.Polygon` - A COMPAS polygon. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polygon: Polygon, **kwargs: Any): - super().__init__(geometry=polygon, **kwargs) + """Scene object for drawing polygons in Blender.""" def draw( self, diff --git a/src/compas_blender/scene/polyhedronobject.py b/src/compas_blender/scene/polyhedronobject.py index 8e0384087cc..60e77304a92 100644 --- a/src/compas_blender/scene/polyhedronobject.py +++ b/src/compas_blender/scene/polyhedronobject.py @@ -1,10 +1,8 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Polyhedron from compas.scene import GeometryObject from compas_blender import conversions @@ -12,19 +10,7 @@ class PolyhedronObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing polyhedron shapes in Blender. - - Parameters - ---------- - polyhedron : :class:`compas.geometry.Polyhedron` - A COMPAS polyhedron. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polyhedron: Polyhedron, **kwargs: Any): - super().__init__(geometry=polyhedron, **kwargs) + """Scene object for drawing polyhedron shapes in Blender.""" 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 scene object. diff --git a/src/compas_blender/scene/polylineobject.py b/src/compas_blender/scene/polylineobject.py index 51ec93c40d1..d21ffc919d9 100644 --- a/src/compas_blender/scene/polylineobject.py +++ b/src/compas_blender/scene/polylineobject.py @@ -1,10 +1,8 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Polyline from compas.scene import GeometryObject from compas_blender import conversions @@ -12,19 +10,7 @@ class PolylineObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing polylines in Blender. - - Parameters - ---------- - polyline : :class:`compas.geometry.Polyline` - A COMPAS polyline. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polyline: Polyline, **kwargs: Any): - super().__init__(geometry=polyline, **kwargs) + """Scene object for drawing polylines in Blender.""" def draw(self, color: Optional[Color] = None, collection: Optional[str] = None) -> list[bpy.types.Object]: """Draw the line. diff --git a/src/compas_blender/scene/sceneobject.py b/src/compas_blender/scene/sceneobject.py index 6edf60dfe64..f5416c1ae71 100644 --- a/src/compas_blender/scene/sceneobject.py +++ b/src/compas_blender/scene/sceneobject.py @@ -18,6 +18,8 @@ class BlenderSceneObject(SceneObject): ---------- collection : str | :blender:`bpy.types.Collection`, optional The Blender scene collection the object(s) created by the scene object belong to. + show_wire : bool, optional + Display the wireframe of the object. **kwargs : dict, optional Additional keyword arguments. @@ -25,12 +27,18 @@ class BlenderSceneObject(SceneObject): ---------- objects : list[:blender:`bpy.types.Object`] The Blender objects created by the scene object. + collection : str | :blender:`bpy.types.Collection` + The Blender scene collection the object(s) created by the scene object belong to. + show_wire : bool + Display the wireframe of the object. """ - def __init__(self, **kwargs: Any): + def __init__(self, collection: Union[str, bpy.types.Collection] = None, show_wire: bool = True, **kwargs: Any): super().__init__(**kwargs) self.objects = [] + self.collection = collection + self.show_wire = show_wire # 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 diff --git a/src/compas_blender/scene/shapeobject.py b/src/compas_blender/scene/shapeobject.py new file mode 100644 index 00000000000..b12c659f8de --- /dev/null +++ b/src/compas_blender/scene/shapeobject.py @@ -0,0 +1,75 @@ +from typing import Any +from typing import Optional + +import bpy # type: ignore + +from compas.geometry import Shape +from compas.scene import GeometryObject +from compas_blender import conversions + +from .sceneobject import BlenderSceneObject + + +class ShapeObject(BlenderSceneObject, GeometryObject): + """Scene object for drawing capsule shapes in Blender. + + Parameters + ---------- + v : int, optional + The number of vertices in the u-direction of non-OCC geometries. + u : int, optional + The number of vertices in the v-direction of non-OCC geometries. + **kwargs : dict, optional + Additional keyword arguments. + + """ + + def __init__( + self, + u: Optional[int] = 16, + v: Optional[int] = 16, + shade_smooth: bool = True, + **kwargs: Any, + ): + super().__init__(**kwargs) + self.geometry: Shape + self.u = u + self.v = v + self.shade_smooth = shade_smooth + + @property + def u(self) -> int: + return self.geometry.resolution_u + + @u.setter + def u(self, u: int) -> None: + self.geometry.resolution_u = u + + @property + def v(self) -> int: + return self.geometry.resolution_v + + @v.setter + def v(self, v: int) -> None: + self.geometry.resolution_v = v + + def draw(self) -> list[bpy.types.Object]: + """Draw the cone associated with the scene object. + + Returns + ------- + list[:blender:`bpy.types.Object`] + The objects created in Blender. + + """ + mesh = conversions.vertices_and_faces_to_blender_mesh(self.geometry.vertices, self.geometry.faces, name=self.geometry.name) + if self.shade_smooth: + mesh.shade_smooth() + else: + mesh.shade_flat() + + obj = self.create_object(mesh, name=self.geometry.name) + self.update_object(obj, color=self.color, collection=self.collection, show_wire=self.show_wire) + + self._guids = [obj] + return self.guids diff --git a/src/compas_blender/scene/sphereobject.py b/src/compas_blender/scene/sphereobject.py deleted file mode 100644 index 23faba0974f..00000000000 --- a/src/compas_blender/scene/sphereobject.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Any -from typing import Optional - -import bpy # type: ignore - -from compas.colors import Color -from compas.geometry import Sphere -from compas.scene import GeometryObject -from compas_blender import conversions - -from .sceneobject import BlenderSceneObject - - -class SphereObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing sphere shapes in Blender. - - Parameters - ---------- - sphere : :class:`compas.geometry.Sphere` - A COMPAS sphere. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - 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 scene object. - - Parameters - ---------- - color : tuple[float, float, float] | tuple[int, int, int] | :class:`compas.colors.Color`, optional - The RGB color of the sphere. - 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. - v : int, optional - Number of faces in the "v" direction. - show_wire : bool, optional - Display the wireframe of the sphere. - shade_smooth : bool, optional - Display smooth shading on the sphere. - - Returns - ------- - list[:blender:`bpy.types.Object`] - The objects created in Blender. - """ - name = self.geometry.name - color = Color.coerce(color) or self.color - - 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) - - self._guids = [obj] - return self.guids diff --git a/src/compas_blender/scene/surfaceobject.py b/src/compas_blender/scene/surfaceobject.py index 94160250b9e..8b0ba37103a 100644 --- a/src/compas_blender/scene/surfaceobject.py +++ b/src/compas_blender/scene/surfaceobject.py @@ -1,10 +1,8 @@ -from typing import Any from typing import Optional import bpy # type: ignore from compas.colors import Color -from compas.geometry import Surface from compas.scene import GeometryObject from compas_blender import conversions @@ -12,19 +10,7 @@ class SurfaceObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing surfaces in Blender. - - Parameters - ---------- - surface : :class:`compas.geometry.Surface` - A COMPAS surface. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, surface: Surface, **kwargs: Any): - super().__init__(geometry=surface, **kwargs) + """Scene object for drawing surfaces in Blender.""" def draw(self, color: Optional[Color] = None, collection: Optional[str] = None) -> list[bpy.types.Object]: """Draw the surface. diff --git a/src/compas_blender/scene/torusobject.py b/src/compas_blender/scene/torusobject.py deleted file mode 100644 index afffad83505..00000000000 --- a/src/compas_blender/scene/torusobject.py +++ /dev/null @@ -1,73 +0,0 @@ -from typing import Any -from typing import Optional - -import bpy # type: ignore - -from compas.colors import Color -from compas.geometry import Torus -from compas.scene import GeometryObject -from compas_blender import conversions - -from .sceneobject import BlenderSceneObject - - -class TorusObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing torus shapes in Blender. - - Parameters - ---------- - torus : :class:`compas.geometry.Torus` - A COMPAS torus. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, torus: Torus, **kwargs: Any): - super().__init__(geometry=torus, **kwargs) - - def draw( - self, - color: Optional[Color] = None, - collection: Optional[str] = None, - u: Optional[int] = 16, - v: Optional[int] = 16, - show_wire: bool = False, - shade_smooth: bool = True, - ) -> list[bpy.types.Object]: - """Draw the torus associated with the scene object. - - Parameters - ---------- - color : tuple[float, float, float] | tuple[int, int, int] | :class:`compas.colors.Color`, optional - The RGB color of the torus. - 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. - v : int, optional - Number of faces in the "v" direction. - show_wire : bool, optional - Display the wireframe of the torus. - shade_smooth : bool, optional - Display smooth shading on the torus. - - Returns - ------- - list[:blender:`bpy.types.Curve`] - The objects created in Blender. - - """ - name = self.geometry.name - color = Color.coerce(color) or self.color - - 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) - - self._guids = [obj] - return self.guids diff --git a/src/compas_blender/scene/vectorobject.py b/src/compas_blender/scene/vectorobject.py index 599434f3d0a..8d962b67079 100644 --- a/src/compas_blender/scene/vectorobject.py +++ b/src/compas_blender/scene/vectorobject.py @@ -1,4 +1,3 @@ -from typing import Any from typing import Optional import bpy # type: ignore @@ -6,7 +5,6 @@ from compas.colors import Color from compas.geometry import Line from compas.geometry import Point -from compas.geometry import Vector from compas.scene import GeometryObject from compas_blender import conversions @@ -14,19 +12,7 @@ class VectorObject(BlenderSceneObject, GeometryObject): - """Scene object for drawing vectors in Blender. - - Parameters - ---------- - primitive : :class:`compas.geometry.Vector` - A COMPAS vector. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, vector: Vector, **kwargs: Any): - super().__init__(geometry=vector, **kwargs) + """Scene object for drawing vectors in Blender.""" def draw( self, diff --git a/src/compas_blender/scene/volmeshobject.py b/src/compas_blender/scene/volmeshobject.py index 545a253c132..5b8f4ff4bb7 100644 --- a/src/compas_blender/scene/volmeshobject.py +++ b/src/compas_blender/scene/volmeshobject.py @@ -1,18 +1,12 @@ from typing import Any -from typing import Dict from typing import List -from typing import Optional -from typing import Tuple -from typing import Union import bpy # type: ignore import compas_blender -from compas.colors import Color +import compas_blender.objects from compas.geometry import Line -from compas.geometry import add_vectors -from compas.geometry import centroid_points -from compas.geometry import scale_vector +from compas.geometry import Sphere from compas.scene import VolMeshObject as BaseVolMeshObject from compas_blender import conversions @@ -24,26 +18,47 @@ class VolMeshObject(BlenderSceneObject, BaseVolMeshObject): Parameters ---------- - volmesh : :class:`compas.datastructures.VolMesh` - The volmesh data structure. + vertex_u : int, optional + Number of segments in the U direction of the vertex spheres. + Default is ``16``. + vertex_v : int, optional + Number of segments in the V direction of the vertex spheres. + Default is ``16``. **kwargs : dict, optional Additional keyword arguments. + Attributes + ---------- + vertex_u : int + Number of segments in the U direction of the vertex spheres. + vertex_v : int + Number of segments in the V direction of the vertex spheres. + vertexobjects : list[:class:`bpy.types.Object`] + The Blender objects representing the vertices. + edgeobjects : list[:class:`bpy.types.Object`] + The Blender objects representing the edges. + faceobjects : list[:class:`bpy.types.Object`] + The Blender objects representing the faces. + cellobjects : list[:class:`bpy.types.Object`] + The Blender objects representing the cells. + """ - def __init__(self, volmesh, **kwargs: Any): - super().__init__(volmesh=volmesh, **kwargs) + def __init__(self, vertex_u=16, vertex_v=16, **kwargs: Any): + super().__init__(**kwargs) self.vertexobjects = [] self.edgeobjects = [] self.faceobjects = [] self.cellobjects = [] + self.vertex_u = vertex_u + self.vertex_v = vertex_v # ========================================================================== # clear # ========================================================================== def clear(self): - compas_blender.delete_objects(self.objects) + compas_blender.objects.delete_objects(self.objects) def clear_vertices(self): """Clear the objects contained in the vertex collection (``self.vertexcollection``). @@ -53,7 +68,7 @@ def clear_vertices(self): None """ - compas_blender.delete_objects(self.vertexobjects) + compas_blender.objects.delete_objects(self.vertexobjects) def clear_edges(self): """Clear the objects contained in the edge collection (``self.edgecollection``). @@ -63,7 +78,7 @@ def clear_edges(self): None """ - compas_blender.delete_objects(self.edgeobjects) + compas_blender.objects.delete_objects(self.edgeobjects) def clear_faces(self): """Clear the objects contained in the face collection (``self.facecollection``). @@ -73,7 +88,7 @@ def clear_faces(self): None """ - compas_blender.delete_objects(self.faceobjects) + compas_blender.objects.delete_objects(self.faceobjects) def clear_cells(self): """Clear the objects contained in the cell collection (``self.cellcollection``). @@ -83,62 +98,39 @@ def clear_cells(self): None """ - compas_blender.delete_objects(self.cellobjects) + compas_blender.objects.delete_objects(self.cellobjects) # ========================================================================== # draw # ========================================================================== - def draw(self, cells: Optional[List[int]] = None, color: Optional[Color] = None, collection: Optional[str] = None) -> list[bpy.types.Object]: + def draw(self) -> list[bpy.types.Object]: """Draw a selection of cells. - Parameters - ---------- - cells : list[int], optional - A list of cells to draw. - 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:`VolMeshObject.default_cellcolor`. - 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. """ - self._guids = self.draw_cells(cells=cells, color=color, collection=collection) + guids = [] + + if self.show_vertices: + guids += self.draw_vertices() + if self.show_edges: + guids += self.draw_edges() + if self.show_faces: + guids += self.draw_faces() + if self.show_cells: + guids += self.draw_cells() + + self._guids = guids + return self.guids - 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]: + def draw_vertices(self) -> List[bpy.types.Object]: """Draw a selection of vertices. - Parameters - ---------- - vertices : list[int], optional - A list of vertex keys identifying which 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. - 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`] @@ -146,40 +138,27 @@ def draw_vertices( """ objects = [] - self.vertexcolor = color + vertices = list(self.volmesh.vertices()) if self.show_vertices is True else self.show_vertices or [] - for vertex in vertices or self.volmesh.vertices(): # type: ignore - name = f"{self.volmesh.name}.vertex.{vertex}" # type: ignore - color = self.vertexcolor[vertex] # type: ignore - point = self.volmesh.vertices_attributes("xyz")[vertex] + for vertex in vertices: + name = f"{self.volmesh.name}.vertex.{vertex}" + color = self.vertexcolor[vertex] + point = self.volmesh.vertex_coordinates(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 + sphere = Sphere(radius=self.vertexsize, point=point) + sphere.resolution_u = self.vertex_u + sphere.resolution_v = self.vertex_v + mesh = conversions.vertices_and_faces_to_blender_mesh(sphere.vertices, sphere.faces, name=name) + obj = self.create_object(mesh, name=name) + self.update_object(obj, color=color, collection=self.collection) objects.append(obj) + self.vertexobjects = objects 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]: + def draw_edges(self) -> List[bpy.types.Object]: """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 : :class:`compas.colors.Color` | dict[tuple[int, int], :class:`compas.colors.Color`], optional - The color specification for the edges. - collection : str, optional - The name of the Blender scene collection containing the created object(s). - Returns ------- list[:blender:`bpy.types.Object`] @@ -187,40 +166,23 @@ def draw_edges( """ objects = [] - self.edgecolor = color + edges = list(self.volmesh.edges()) if self.show_edges is True else self.show_edges or [] - for u, v in edges or self.volmesh.edges(): # type: ignore - name = f"{self.volmesh.name}.edge.{u}-{v}" # type: ignore - color = self.edgecolor[u, v] # type: ignore - curve = conversions.line_to_blender_curve(Line(self.volmesh.vertices_attributes("xyz")[u], self.volmesh.vertices_attributes("xyz")[v])) + for u, v in edges: + name = f"{self.volmesh.name}.edge.{u}-{v}" + color = self.edgecolor[u, v] + curve = conversions.line_to_blender_curve(Line(self.volmesh.vertex_coordinates(u), self.volmesh.vertex_coordinates(v))) obj = self.create_object(curve, name=name) - self.update_object(obj, color=color, collection=collection) # type: ignore + self.update_object(obj, color=color, collection=self.collection) objects.append(obj) + self.edgeobjects = objects 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]: + def draw_faces(self) -> List[bpy.types.Object]: """Draw a selection of faces. - Parameters - ---------- - faces : list[int], optional - A list of face keys identifying which 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. - 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`] @@ -228,41 +190,24 @@ def draw_faces( """ objects = [] - self.facecolor = color + faces = list(self.volmesh.faces()) if self.show_faces is True else self.show_faces or [] - for face in faces or self.volmesh.faces(): # type: ignore - name = f"{self.volmesh.name}.face.{face}" # type: ignore - color = self.facecolor[face] # type: ignore - points = [self.volmesh.vertices_attributes("xyz")[vertex] for vertex in self.volmesh.face_vertices(face)] # type: ignore - mesh = conversions.polygon_to_blender_mesh(points, name=name) # type: ignore + for face in faces: + name = f"{self.volmesh.name}.face.{face}" + color = self.facecolor[face] + points = [self.volmesh.vertex_coordinates(vertex) for vertex in self.volmesh.face_vertices(face)] + mesh = conversions.polygon_to_blender_mesh(points, name=name) obj = self.create_object(mesh, name=name) - self.update_object(obj, color=color, collection=collection, show_wire=show_wire) # type: ignore + self.update_object(obj, color=color, collection=self.collection) objects.append(obj) + self.faceobjects = objects 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]: + def draw_cells(self) -> List[bpy.types.Object]: """Draw a selection of cells. - Parameters - ---------- - cells : list[int], optional - A list of cells to draw. - 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. - 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`] @@ -270,23 +215,23 @@ def draw_cells( """ objects = [] - self.cellcolor = color + cells = list(self.volmesh.cells()) if self.show_cells is True else self.show_cells or [] - for cell in cells or self.volmesh.cells(): # type: ignore - name = f"{self.volmesh.name}.cell.{cell}" # type: ignore - color = self.cellcolor[cell] # type: ignore + for cell in cells: + name = "{}.cell.{}".format(self.volmesh.name, cell) + color = self.cellcolor[cell] - vertices = self.volmesh.cell_vertices(cell) # type: ignore - faces = self.volmesh.cell_faces(cell) # type: ignore + vertices = self.volmesh.cell_vertices(cell) + faces = self.volmesh.cell_faces(cell) vertex_index = dict((vertex, index) for index, vertex in enumerate(vertices)) - vertices = [self.volmesh.vertices_attributes("xyz")[vertex] for vertex in vertices] - faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] # type: ignore + vertices = [self.volmesh.vertex_coordinates(vertex) for vertex in vertices] + faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] - mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=name) # type: ignore + mesh = conversions.vertices_and_faces_to_blender_mesh(vertices, faces, name=name) obj = self.create_object(mesh, name=name) - self.update_object(obj, color=color, collection=collection, show_wire=show_wire) # type: ignore + self.update_object(obj, color=color, collection=self.collection, show_wire=self.show_wire) objects.append(obj) @@ -296,180 +241,94 @@ def draw_cells( # draw normals # ========================================================================== - def draw_vertexnormals( - self, - 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. - - Parameters - ---------- - vertices : list[int], optional - A selection of vertex normals to draw. - Default is to draw all vertex normals. - color : :class:`compas.colors.Color`, optional - 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`] - - """ - 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.volmesh.vertices_attributes("xyz")[vertex] - n = self.volmesh.vertex_normal(vertex) # type: ignore - b = add_vectors(a, scale_vector(n, scale)) - - 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. - - Parameters - ---------- - faces : list[int], optional - A selection of face normals to draw. - Default is to draw all face normals. - color : :class:`compas.colors.Color`, optional - 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). + # def draw_vertexnormals( + # self, + # 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. - Returns - ------- - list[:blender:`bpy.types.Object`] + # Parameters + # ---------- + # vertices : list[int], optional + # A selection of vertex normals to draw. + # Default is to draw all vertex normals. + # color : :class:`compas.colors.Color`, optional + # 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). - """ - objects = [] + # Returns + # ------- + # list[:blender:`bpy.types.Object`] - color = Color.coerce(color) # type: ignore + # """ + # objects = [] - for face in faces or self.volmesh.faces(): # type: ignore - name = f"{self.volmesh.name}.face.{face}.normal" # type: ignore + # color = Color.coerce(color) # type: ignore - a = centroid_points([self.volmesh.vertices_attributes("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)) + # for vertex in vertices or self.volmesh.vertices(): # type: ignore + # name = f"{self.volmesh.name}.vertex.{vertex}.normal" # type: ignore - curve = conversions.line_to_blender_curve(Line(a, b)) + # a = self.volmesh.vertices_attributes("xyz")[vertex] + # n = self.volmesh.vertex_normal(vertex) # type: ignore + # b = add_vectors(a, scale_vector(n, scale)) - obj = self.create_object(curve, name=name) - self.update_object(obj, color=color, collection=collection) + # curve = conversions.line_to_blender_curve(Line(a, b)) - objects.append(obj) + # obj = self.create_object(curve, name=name) + # self.update_object(obj, color=color, collection=collection) - return objects + # objects.append(obj) - # ========================================================================== - # draw labels - # ========================================================================== + # return objects - # def draw_vertexlabels(self, text: Optional[Dict[int, str]] = None) -> List[bpy.types.Object]: - # """Draw labels for a selection vertices. + # 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. # 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. + # faces : list[int], optional + # A selection of face normals to draw. + # Default is to draw all face normals. + # color : :class:`compas.colors.Color`, optional + # 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`] # """ - # self.vertex_text = text - # labels = [] - # for vertex in self.vertex_text: - # labels.append( - # { - # "pos": self.volmesh.vertices_attributes("xyz")[vertex], - # "name": f"{self.volmesh.name}.vertexlabel.{vertex}", - # "text": self.vertex_text[vertex], - # "color": self.vertexcolor[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. + # color = Color.coerce(color) # type: ignore - # Returns - # ------- - # list[:blender:`bpy.types.Object`] + # for face in faces or self.volmesh.faces(): # type: ignore + # name = f"{self.volmesh.name}.face.{face}.normal" # type: ignore - # """ - # self.edge_text = text - # labels = [] - # for edge in self.edge_text: - # u, v = edge - # labels.append( - # { - # "pos": centroid_points([self.volmesh.vertices_attributes("xyz")[u], self.volmesh.vertices_attributes("xyz")[v]]), - # "name": f"{self.volmesh.name}.edgelabel.{u}-{v}", - # "text": self.edge_text[edge], - # "color": self.edgecolor[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. + # a = centroid_points([self.volmesh.vertices_attributes("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)) - # 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. + # curve = conversions.line_to_blender_curve(Line(a, b)) - # Returns - # ------- - # list[:blender:`bpy.types.Object`] + # obj = self.create_object(curve, name=name) + # self.update_object(obj, color=color, collection=collection) - # """ - # self.face_text = text - # labels = [] - # for face in self.face_text: - # labels.append( - # { - # "pos": centroid_points([self.volmesh.vertices_attributes("xyz")[vertex] for vertex in self.volmesh.face_vertices(face)]), - # "name": "{}.facelabel.{}".format(self.volmesh.name, face), - # "text": self.face_text[face], - # "color": self.facecolor[face], - # } - # ) - # return compas_blender.draw_texts(labels, collection=self.collection) + # objects.append(obj) + + # return objects diff --git a/src/compas_blender/utilities/__init__.py b/src/compas_blender/utilities/__init__.py index c018d710e46..10dd64eb287 100644 --- a/src/compas_blender/utilities/__init__.py +++ b/src/compas_blender/utilities/__init__.py @@ -1,4 +1,6 @@ -from .drawing import ( +from warnings import warn + +from ..drawing import ( draw_circles, draw_cylinders, draw_cubes, @@ -36,3 +38,5 @@ "draw_surfaces", "RGBColor", ] + +warn("compas_blender.utilities will be removed in version 2.3. Please use compas_blender.drawing instead.", DeprecationWarning, stacklevel=2) diff --git a/src/compas_ghpython/utilities/drawing.py b/src/compas_ghpython/drawing.py similarity index 94% rename from src/compas_ghpython/utilities/drawing.py rename to src/compas_ghpython/drawing.py index ddd993040a8..001fa4e3346 100644 --- a/src/compas_ghpython/utilities/drawing.py +++ b/src/compas_ghpython/drawing.py @@ -18,13 +18,10 @@ from Rhino.Geometry import Sphere from Rhino.Geometry import Vector3d from Rhino.Geometry import Vector3f -from System.Array import CreateInstance -from System.Drawing import Color -from System.Enum import ToObject from compas.geometry import centroid_points from compas.itertools import pairwise -from compas_rhino.utilities.drawing import _face_to_max_quad +from compas_rhino.drawing import _face_to_max_quad try: from Rhino.Geometry import MeshNgon @@ -284,7 +281,7 @@ def draw_pipes(pipes, cap=2, fit=1.0): points = p["points"] radius = p["radius"] params = [0.0, 1.0] - cap = ToObject(PipeCapMode, cap) + cap = PipeCapMode(cap) if type(radius) in (int, float): radius = [radius] * 2 radius = [float(r) for r in radius] @@ -366,23 +363,17 @@ def draw_mesh(vertices, faces, color=None, vertex_normals=None, texture_coordina 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]) + normals = [Vector3f(normal[0], normal[1], normal[2]) for normal in vertex_normals] mesh.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]) + tcs = [Point2f(tc[0], tc[1]) for tc in texture_coordinates] mesh.TextureCoordinates.SetTextureCoordinates(tcs) if color: count = len(mesh.Vertices) - colors = CreateInstance(Color, count) - for i in range(count): - colors[i] = rs.coercecolor(color) + colors = [rs.coercecolor(color) for i in range(count)] mesh.VertexColors.SetColors(colors) return mesh diff --git a/src/compas_ghpython/scene/__init__.py b/src/compas_ghpython/scene/__init__.py index 1dceb254b72..822d5220a37 100644 --- a/src/compas_ghpython/scene/__init__.py +++ b/src/compas_ghpython/scene/__init__.py @@ -57,9 +57,9 @@ from .brepobject import BrepObject -@plugin(category="drawing-utils", pluggable_name="clear", requires=["Grasshopper"]) -def clear_GH(guids=None): - pass +# @plugin(category="drawing-utils", pluggable_name="clear", requires=["Grasshopper"]) +# def clear_GH(guids=None): +# pass @plugin(category="factories", requires=["Rhino"]) diff --git a/src/compas_ghpython/scene/boxobject.py b/src/compas_ghpython/scene/boxobject.py index 5cc694f9b41..21bedc66df7 100644 --- a/src/compas_ghpython/scene/boxobject.py +++ b/src/compas_ghpython/scene/boxobject.py @@ -9,19 +9,7 @@ class BoxObject(GHSceneObject, GeometryObject): - """Scene object for drawing box shapes. - - Parameters - ---------- - box : :class:`compas.geometry.Box` - A COMPAS box. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, box, **kwargs): - super(BoxObject, self).__init__(geometry=box, **kwargs) + """Scene object for drawing box shapes.""" def draw(self): """Draw the box associated with the scene object. diff --git a/src/compas_ghpython/scene/brepobject.py b/src/compas_ghpython/scene/brepobject.py index 939dd4e5bbf..a4ba1cdf755 100644 --- a/src/compas_ghpython/scene/brepobject.py +++ b/src/compas_ghpython/scene/brepobject.py @@ -9,19 +9,7 @@ class BrepObject(GHSceneObject, GeometryObject): - """A Scene object 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(BrepObject, self).__init__(geometry=brep, **kwargs) + """A Scene object for drawing a brep in Grasshopper.""" def draw(self): """Draw the brep as a Grasshopper geometry. diff --git a/src/compas_ghpython/scene/capsuleobject.py b/src/compas_ghpython/scene/capsuleobject.py index 25518e63dcd..4f243d273f4 100644 --- a/src/compas_ghpython/scene/capsuleobject.py +++ b/src/compas_ghpython/scene/capsuleobject.py @@ -9,19 +9,7 @@ class CapsuleObject(GHSceneObject, GeometryObject): - """Scene object for drawing capsule shapes. - - Parameters - ---------- - capsule : :class:`compas.geometry.Capsule` - A COMPAS capsule. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, capsule, **kwargs): - super(CapsuleObject, self).__init__(geometry=capsule, **kwargs) + """Scene object for drawing capsule shapes.""" def draw(self): """Draw the capsule associated with the scene object. diff --git a/src/compas_ghpython/scene/circleobject.py b/src/compas_ghpython/scene/circleobject.py index f74fb3e0dba..ee8a16e81f7 100644 --- a/src/compas_ghpython/scene/circleobject.py +++ b/src/compas_ghpython/scene/circleobject.py @@ -9,19 +9,7 @@ class CircleObject(GHSceneObject, GeometryObject): - """Scene object for drawing circles. - - Parameters - ---------- - circle : :class:`compas.geometry.Circle` - A COMPAS circle. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, circle, **kwargs): - super(CircleObject, self).__init__(geometry=circle, **kwargs) + """Scene object for drawing circles.""" def draw(self): """Draw the circle. diff --git a/src/compas_ghpython/scene/coneobject.py b/src/compas_ghpython/scene/coneobject.py index c19a318ac8a..621aac32479 100644 --- a/src/compas_ghpython/scene/coneobject.py +++ b/src/compas_ghpython/scene/coneobject.py @@ -9,19 +9,7 @@ class ConeObject(GHSceneObject, GeometryObject): - """Scene object for drawing cone shapes. - - Parameters - ---------- - shape : :class:`compas.geometry.Cone` - A COMPAS cone. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, cone, **kwargs): - super(ConeObject, self).__init__(geometry=cone, **kwargs) + """Scene object for drawing cone shapes.""" def draw(self): """Draw the cone associated with the scene object. diff --git a/src/compas_ghpython/scene/curveobject.py b/src/compas_ghpython/scene/curveobject.py index ede00aaaaea..1bac2aedcd8 100644 --- a/src/compas_ghpython/scene/curveobject.py +++ b/src/compas_ghpython/scene/curveobject.py @@ -9,22 +9,7 @@ class CurveObject(GHSceneObject, GeometryObject): - """Scene object for drawing curves. - - Parameters - ---------- - curve : :class:`compas.geometry.Curve` - A COMPAS curve. - - Other Parameters - ---------------- - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, curve, **kwargs): - super(CurveObject, self).__init__(geometry=curve, **kwargs) + """Scene object for drawing curves.""" def draw(self): """Draw the curve. diff --git a/src/compas_ghpython/scene/cylinderobject.py b/src/compas_ghpython/scene/cylinderobject.py index 19f26846682..f8cc986723f 100644 --- a/src/compas_ghpython/scene/cylinderobject.py +++ b/src/compas_ghpython/scene/cylinderobject.py @@ -9,19 +9,7 @@ class CylinderObject(GHSceneObject, GeometryObject): - """Scene object for drawing cylinder shapes. - - Parameters - ---------- - cylinder : :class:`compas.geometry.Cylinder` - A COMPAS cylinder. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, cylinder, **kwargs): - super(CylinderObject, self).__init__(geometry=cylinder, **kwargs) + """Scene object for drawing cylinder shapes.""" def draw(self): """Draw the cylinder associated with the scene object. @@ -30,6 +18,7 @@ def draw(self): ------- list[:rhino:`Rhino.Geometry.Brep`] List of created Rhino breps. + """ geometry = conversions.cylinder_to_rhino_brep(self.geometry) geometry.Transform(conversions.transformation_to_rhino(self.worldtransformation)) diff --git a/src/compas_ghpython/scene/ellipseobject.py b/src/compas_ghpython/scene/ellipseobject.py index e32a4465305..3c7263fe506 100644 --- a/src/compas_ghpython/scene/ellipseobject.py +++ b/src/compas_ghpython/scene/ellipseobject.py @@ -9,19 +9,7 @@ class EllipseObject(GHSceneObject, GeometryObject): - """Scene object for drawing ellipses. - - Parameters - ---------- - ellipse : :class:`compas.geometry.Ellipse` - A COMPAS ellipse. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, ellipse, **kwargs): - super(EllipseObject, self).__init__(geometry=ellipse, **kwargs) + """Scene object for drawing ellipses.""" def draw(self): """Draw the ellipse. diff --git a/src/compas_ghpython/scene/frameobject.py b/src/compas_ghpython/scene/frameobject.py index 26eaf85cbb6..dde68046f42 100644 --- a/src/compas_ghpython/scene/frameobject.py +++ b/src/compas_ghpython/scene/frameobject.py @@ -13,8 +13,6 @@ class FrameObject(GHSceneObject, GeometryObject): Parameters ---------- - frame : :class:`compas.geometry.Frame` - A COMPAS frame. scale : float, optional The scale of the vectors representing the axes of the frame. **kwargs : dict, optional @@ -24,19 +22,11 @@ class FrameObject(GHSceneObject, GeometryObject): ---------- scale : float Scale factor that controls the length of the axes. - color_origin : :class:`compas.colors.Color` - Default is ``Color.black()``. - color_xaxis : :class:`compas.colors.Color` - Default is ``Color.red()``. - color_yaxis : :class:`compas.colors.Color` - Default is ``Color.green()``. - color_zaxis : :class:`compas.colors.Color` - Default is ``Color.blue()``. """ - def __init__(self, frame, scale=1.0, **kwargs): - super(FrameObject, self).__init__(geometry=frame, **kwargs) + def __init__(self, scale=1.0, **kwargs): + super(FrameObject, self).__init__(**kwargs) self.scale = scale def draw(self): @@ -46,6 +36,7 @@ def draw(self): ------- list[:rhino:`Rhino.Geometry.Point3d`, :rhino:`Rhino.Geometry.Line`] List of created Rhino geometries. + """ geometries = [] diff --git a/src/compas_ghpython/scene/graphobject.py b/src/compas_ghpython/scene/graphobject.py index ac9396b651e..7a0ef394f3f 100644 --- a/src/compas_ghpython/scene/graphobject.py +++ b/src/compas_ghpython/scene/graphobject.py @@ -9,19 +9,7 @@ class GraphObject(GHSceneObject, BaseGraphObject): - """Scene object for drawing graph data structures. - - Parameters - ---------- - graph : :class:`compas.datastructures.Graph` - A COMPAS graph. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, graph, **kwargs): - super(GraphObject, self).__init__(graph=graph, **kwargs) + """Scene object for drawing graph data structures.""" def draw(self): """Draw the entire graph with default color settings. @@ -34,15 +22,9 @@ def draw(self): self._guids = self.draw_edges() + self.draw_nodes() return self.guids - def draw_nodes(self, nodes=None): + def draw_nodes(self): """Draw a selection of nodes. - Parameters - ---------- - nodes: list[hashable], optional - The selection of nodes that should be drawn. - Default is None, in which case all nodes are drawn. - Returns ------- list[:rhino:`Rhino.Geometry.Point3d`] @@ -50,7 +32,9 @@ def draw_nodes(self, nodes=None): """ points = [] - for node in nodes or self.graph.nodes(): # type: ignore + nodes = list(self.graph.nodes()) if self.show_nodes is True else self.show_nodes or [] + + for node in nodes: points.append(conversions.point_to_rhino(self.node_xyz[node])) return points @@ -58,12 +42,6 @@ def draw_nodes(self, nodes=None): def draw_edges(self, edges=None): """Draw a selection of edges. - Parameters - ---------- - edges : list[tuple[hashable, hashable]], optional - A list of edges to draw. - The default is None, in which case all edges are drawn. - Returns ------- list[:rhino:`Rhino.Geometry.Line`] @@ -71,7 +49,9 @@ def draw_edges(self, edges=None): """ lines = [] - for edge in edges or self.graph.edges(): # type: ignore + edges = list(self.graph.edges()) if self.show_edges is True else self.show_edges or [] + + for edge in edges: lines.append(conversions.line_to_rhino((self.node_xyz[edge[0]], self.node_xyz[edge[1]]))) return lines diff --git a/src/compas_ghpython/scene/lineobject.py b/src/compas_ghpython/scene/lineobject.py index ce838408a13..86159db3c01 100644 --- a/src/compas_ghpython/scene/lineobject.py +++ b/src/compas_ghpython/scene/lineobject.py @@ -9,19 +9,7 @@ class LineObject(GHSceneObject, GeometryObject): - """Scene object for drawing lines. - - Parameters - ---------- - line : :class:`compas.geometry.Line` - A COMPAS line. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, line, **kwargs): - super(LineObject, self).__init__(geometry=line, **kwargs) + """Scene object for drawing lines.""" def draw(self): """Draw the line. diff --git a/src/compas_ghpython/scene/meshobject.py b/src/compas_ghpython/scene/meshobject.py index d4bcd6a0182..46f4aada794 100644 --- a/src/compas_ghpython/scene/meshobject.py +++ b/src/compas_ghpython/scene/meshobject.py @@ -2,7 +2,6 @@ from __future__ import division from __future__ import print_function -from compas.colors import Color from compas.scene import MeshObject as BaseMeshObject from compas_rhino import conversions from compas_rhino.scene.helpers import ngon @@ -15,17 +14,23 @@ class MeshObject(GHSceneObject, BaseMeshObject): Parameters ---------- - mesh : :class:`compas.datastructures.Mesh` - A COMPAS mesh. + disjoint : bool, optional + Draw the mesh as disjoint faces. **kwargs : dict, optional Additional keyword arguments. + Attributes + ---------- + disjoint : bool + Draw the mesh as disjoint faces. + """ - def __init__(self, mesh, **kwargs): - super(MeshObject, self).__init__(mesh=mesh, **kwargs) + def __init__(self, disjoint=False, **kwargs): + super(MeshObject, self).__init__(**kwargs) + self.disjoint = disjoint - def draw(self, color=None, vertexcolors=None, facecolors=None, disjoint=False): + def draw(self): """Draw the mesh. Parameters @@ -42,36 +47,54 @@ def draw(self, color=None, vertexcolors=None, facecolors=None, disjoint=False): # 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 + self._guids = [] + + if self.show_faces is True: + vertexcolors = [] + if len(self.vertexcolor): + vertexcolors = [self.vertexcolor[vertex] for vertex in self.mesh.vertices()] + + facecolors = [] + if len(self.facecolor): + facecolors = [self.facecolor[face] for face in self.mesh.faces()] + + color = None + if not vertexcolors and not facecolors: + color = self.color + + vertex_index = self.mesh.vertex_index() + vertex_xyz = self.vertex_xyz - vertex_index = self.mesh.vertex_index() # type: ignore - 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()] - 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 + geometry = conversions.vertices_and_faces_to_rhino( + vertices, + faces, + color=color, + vertexcolors=vertexcolors, + facecolors=facecolors, + disjoint=self.disjoint, + ) - geometry = conversions.vertices_and_faces_to_rhino( - vertices, - faces, - color=color, - vertexcolors=vertexcolors, - facecolors=facecolors, - disjoint=disjoint, - ) + # geometry.Transform(conversions.transformation_to_rhino(self.worldtransformation)) + + self._guids.append(geometry) + + elif self.show_faces: + self._guids += self.draw_faces() + + if self.show_vertices: + self._guids += self.draw_vertices() + + if self.show_edges: + self._guids += self.draw_edges() - self._guids = [geometry] return self.guids - def draw_vertices(self, vertices=None): + def draw_vertices(self): """Draw a selection of vertices. - Parameters - ---------- - vertices : list[int], optional - A selection of vertices to draw. - Default is None, in which case all vertices are drawn. - Returns ------- list[:rhino:`Rhino.Geometry.Point3d`] @@ -79,20 +102,17 @@ def draw_vertices(self, vertices=None): """ points = [] - for vertex in vertices or self.mesh.vertices(): # type: ignore - points.append(conversions.point_to_rhino(self.vertex_xyz[vertex])) + vertices = list(self.mesh.vertices()) if self.show_vertices is True else self.show_vertices or [] + + if vertices: + for vertex in vertices: + points.append(conversions.point_to_rhino(self.vertex_xyz[vertex])) return points - def draw_edges(self, edges=None): + def draw_edges(self): """Draw a selection of edges. - Parameters - ---------- - edges : list[tuple[int, int]], optional - A selection of edges to draw. - The default is None, in which case all edges are drawn. - Returns ------- list[:rhino:`Rhino.Geometry.Line`] @@ -100,38 +120,32 @@ def draw_edges(self, edges=None): """ lines = [] - 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]]))) + edges = list(self.mesh.edges()) if self.show_edges is True else self.show_edges or [] + + if edges: + for edge in edges: + 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): + def draw_faces(self): """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. - Returns ------- list[:rhino:`Rhino.Geometry.Mesh`] """ - faces = faces or self.mesh.faces() # type: ignore - - self.facecolor = color - meshes = [] - for face in faces: - color = self.facecolor[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])) + faces = list(self.mesh.faces()) if self.show_faces is True else self.show_faces or [] + + if faces: + for face in faces: + color = self.facecolor[face] + vertices = [self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)] + facet = ngon(len(vertices)) + if facet: + meshes.append(conversions.vertices_and_faces_to_rhino(vertices, [facet], color=color)) return meshes diff --git a/src/compas_ghpython/scene/planeobject.py b/src/compas_ghpython/scene/planeobject.py index cd2472cf9a2..c483f85ba6d 100644 --- a/src/compas_ghpython/scene/planeobject.py +++ b/src/compas_ghpython/scene/planeobject.py @@ -14,8 +14,6 @@ class PlaneObject(GHSceneObject, GeometryObject): Parameters ---------- - plane : :class:`compas.geometry.Plane` - A COMPAS plane. scale : float, optional Scale factor that controls the visualisation size of the plane. Default is ``1.0``. @@ -29,8 +27,8 @@ class PlaneObject(GHSceneObject, GeometryObject): """ - def __init__(self, plane, scale=1.0, **kwargs): - super(PlaneObject, self).__init__(geometry=plane, **kwargs) + def __init__(self, scale=1.0, **kwargs): + super(PlaneObject, self).__init__(**kwargs) self.scale = scale def draw(self): diff --git a/src/compas_ghpython/scene/pointobject.py b/src/compas_ghpython/scene/pointobject.py index e16c3f6c438..93ec8ef6572 100644 --- a/src/compas_ghpython/scene/pointobject.py +++ b/src/compas_ghpython/scene/pointobject.py @@ -9,19 +9,7 @@ class PointObject(GHSceneObject, GeometryObject): - """Scene object for drawing points. - - Parameters - ---------- - point : :class:`compas.geometry.Point` - A COMPAS point. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, point, **kwargs): - super(PointObject, self).__init__(geometry=point, **kwargs) + """Scene object for drawing points.""" def draw(self): """Draw the point. diff --git a/src/compas_ghpython/scene/polygonobject.py b/src/compas_ghpython/scene/polygonobject.py index 5ca5ab8c9ad..0ac74a96982 100644 --- a/src/compas_ghpython/scene/polygonobject.py +++ b/src/compas_ghpython/scene/polygonobject.py @@ -2,7 +2,6 @@ from __future__ import division from __future__ import print_function -from compas.colors import Color from compas.scene import GeometryObject from compas_rhino import conversions @@ -10,34 +9,17 @@ class PolygonObject(GHSceneObject, GeometryObject): - """Scene object for drawing polygons. + """Scene object for drawing polygons.""" - Parameters - ---------- - polygon : :class:`compas.geometry.Polygon` - A COMPAS polygon. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polygon, **kwargs): - super(PolygonObject, self).__init__(geometry=polygon, **kwargs) - - def draw(self, color=None, show_vertices=False, show_edges=False): + def draw(self): """Draw the polygon. - Parameters - ---------- - color : tuple[int, int, int] | tuple[float, float, float] | :class:`compas.colors.Color`, optional - The RGB color of the polygon. - Returns ------- list[:rhino:`Rhino.Geometry.Mesh`] """ - color = Color.coerce(color) or self.color + color = self.surfacecolor vertices = self.geometry.vertices faces = self.geometry.faces diff --git a/src/compas_ghpython/scene/polyhedronobject.py b/src/compas_ghpython/scene/polyhedronobject.py index 1cf106f37e6..c68331e0ece 100644 --- a/src/compas_ghpython/scene/polyhedronobject.py +++ b/src/compas_ghpython/scene/polyhedronobject.py @@ -2,7 +2,6 @@ from __future__ import division from __future__ import print_function -from compas.colors import Color from compas.scene import GeometryObject from compas_rhino import conversions @@ -10,21 +9,9 @@ class PolyhedronObject(GHSceneObject, GeometryObject): - """Scene object for drawing polyhedron shapes. + """Scene object for drawing polyhedron shapes.""" - Parameters - ---------- - polyhedron : :class:`compas.geometry.Polyhedron` - A COMPAS polyhedron. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polyhedron, **kwargs): - super(PolyhedronObject, self).__init__(geometry=polyhedron, **kwargs) - - def draw(self, color=None): + def draw(self): """Draw the polyhedron associated with the scene object. Parameters @@ -38,7 +25,7 @@ def draw(self, color=None): List of created Rhino mesh. """ - color = Color.coerce(color) or self.color + color = self.surfacecolor vertices, faces = self.geometry.to_vertices_and_faces() geometry = conversions.vertices_and_faces_to_rhino(vertices, faces, color=color) diff --git a/src/compas_ghpython/scene/polylineobject.py b/src/compas_ghpython/scene/polylineobject.py index 86d50d7f9b1..4f3b4c02948 100644 --- a/src/compas_ghpython/scene/polylineobject.py +++ b/src/compas_ghpython/scene/polylineobject.py @@ -9,19 +9,7 @@ class PolylineObject(GHSceneObject, GeometryObject): - """Scene object for drawing polylines. - - Parameters - ---------- - polyline : :class:`compas.geometry.Polyline` - A COMPAS polyline. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polyline, **kwargs): - super(PolylineObject, self).__init__(geometry=polyline, **kwargs) + """Scene object for drawing polylines.""" def draw(self): """Draw the polyline. diff --git a/src/compas_ghpython/scene/sceneobject.py b/src/compas_ghpython/scene/sceneobject.py index 1fde7497460..c4eaa0e9988 100644 --- a/src/compas_ghpython/scene/sceneobject.py +++ b/src/compas_ghpython/scene/sceneobject.py @@ -7,6 +7,3 @@ class GHSceneObject(SceneObject): """Base class for all GH scene objects.""" - - def __init__(self, **kwargs): - super(GHSceneObject, self).__init__(**kwargs) diff --git a/src/compas_ghpython/scene/sphereobject.py b/src/compas_ghpython/scene/sphereobject.py index 702d39d1898..a7ad12aa6f9 100644 --- a/src/compas_ghpython/scene/sphereobject.py +++ b/src/compas_ghpython/scene/sphereobject.py @@ -9,19 +9,7 @@ class SphereObject(GHSceneObject, GeometryObject): - """Scene object for drawing sphere shapes. - - Parameters - ---------- - sphere : :class:`compas.geometry.Sphere` - A COMPAS sphere. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, sphere, **kwargs): - super(SphereObject, self).__init__(geometry=sphere, **kwargs) + """Scene object for drawing sphere shapes.""" def draw(self): """Draw the sphere associated with the scene object. diff --git a/src/compas_ghpython/scene/surfaceobject.py b/src/compas_ghpython/scene/surfaceobject.py index 7d2cf3fd79f..321e8fdb097 100644 --- a/src/compas_ghpython/scene/surfaceobject.py +++ b/src/compas_ghpython/scene/surfaceobject.py @@ -9,22 +9,7 @@ class SurfaceObject(GHSceneObject, GeometryObject): - """Scene object for drawing surfaces. - - Parameters - ---------- - surface : :class:`compas.geometry.Surface` - A COMPAS surface. - - Other Parameters - ---------------- - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, surface, **kwargs): - super(SurfaceObject, self).__init__(geometry=surface, **kwargs) + """Scene object for drawing surfaces.""" def draw(self): """Draw the surface. diff --git a/src/compas_ghpython/scene/torusobject.py b/src/compas_ghpython/scene/torusobject.py index bbb48cd9c88..f5740ca0c65 100644 --- a/src/compas_ghpython/scene/torusobject.py +++ b/src/compas_ghpython/scene/torusobject.py @@ -9,19 +9,7 @@ class TorusObject(GHSceneObject, GeometryObject): - """Scene object for drawing torus shapes. - - Parameters - ---------- - torus : :class:`compas.geometry.Torus` - A COMPAS torus. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, torus, **kwargs): - super(TorusObject, self).__init__(geometry=torus, **kwargs) + """Scene object for drawing torus shapes.""" def draw(self): """Draw the torus associated with the scene object. diff --git a/src/compas_ghpython/scene/vectorobject.py b/src/compas_ghpython/scene/vectorobject.py index aeeff71749b..1055b521889 100644 --- a/src/compas_ghpython/scene/vectorobject.py +++ b/src/compas_ghpython/scene/vectorobject.py @@ -10,21 +10,9 @@ class VectorObject(GHSceneObject, GeometryObject): - """Scene object for drawing vectors. + """Scene object for drawing vectors.""" - Parameters - ---------- - vector : :class:`compas.geometry.Vector` - A COMPAS vector. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, vector, **kwargs): - super(VectorObject, self).__init__(geometry=vector, **kwargs) - - def draw(self, point=None, show_point=False): + def draw(self, point=None): """Draw the vector. Parameters diff --git a/src/compas_ghpython/scene/volmeshobject.py b/src/compas_ghpython/scene/volmeshobject.py index adebfa1e8d1..fdc30a2e04b 100644 --- a/src/compas_ghpython/scene/volmeshobject.py +++ b/src/compas_ghpython/scene/volmeshobject.py @@ -10,50 +10,34 @@ class VolMeshObject(GHSceneObject, BaseVolMeshObject): - """Scene object for drawing volmesh data structures. + """Scene object for drawing volmesh data structures.""" - Parameters - ---------- - volmesh : :class:`compas.datastructures.VolMesh` - A COMPAS volmesh. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, volmesh, **kwargs): - super(VolMeshObject, self).__init__(volmesh=volmesh, **kwargs) - - def draw(self, cells=None, color=None): + def draw(self): """Draw a selection of cells. - Parameters - ---------- - cells : list[int], optional - A list of cells to draw. - 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:`VolMeshObject.default_cellcolor`. - Returns ------- list[:rhino:`Rhino.Geometry.Mesh`] List of created Rhino meshes. """ - self._guids = self.draw_cells(cells=cells, color=color) + guids = [] + + if self.show_vertices: + guids += self.draw_vertices() + if self.show_edges: + guids += self.draw_edges() + if self.show_faces: + guids += self.draw_faces() + if self.show_cells: + guids += self.draw_cells() + + self._guids = guids return self.guids - def draw_vertices(self, vertices=None): + def draw_vertices(self): """Draw a selection of vertices. - Parameters - ---------- - vertices : list - A list of vertices to draw. - Default is None, in which case all vertices are drawn. - Returns ------- list[:rhino:`Rhino.Geometry.Point3d`] @@ -61,20 +45,16 @@ def draw_vertices(self, vertices=None): """ points = [] - for vertex in vertices or self.volmesh.vertices(): # type: ignore + vertices = list(self.volmesh.vertices()) if self.show_vertices is True else self.show_vertices or [] + + for vertex in vertices: points.append(conversions.point_to_rhino(self.vertex_xyz[vertex])) return points - def draw_edges(self, edges=None): + def draw_edges(self): """Draw a selection of edges. - Parameters - ---------- - edges : list[tuple[int, int]], optional - A list of edges to draw. - The default is None, in which case all edges are drawn. - Returns ------- list[:rhino:`Rhino.Geometry.Line`] @@ -82,73 +62,56 @@ def draw_edges(self, edges=None): """ lines = [] - for edge in edges or self.volmesh.edges(): # type: ignore + edges = list(self.volmesh.edges()) if self.show_edges is True else self.show_edges or [] + + for edge in edges: 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): + def draw_faces(self): """Draw a selection of faces. - Parameters - ---------- - faces : list[list[int]], optional - A list 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 :attr:`VolMeshObject.default_facecolor`. - Returns ------- list[:rhino:`Rhino.Geometry.Mesh`] """ - faces = faces or self.volmesh.faces() # type: ignore - - self.facecolor = color - meshes = [] + faces = list(self.volmesh.faces()) if self.show_faces is True else self.show_faces or [] + for face in faces: - color = self.facecolor[face] # type: ignore - vertices = [self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)] # type: ignore + color = self.facecolor[face] + vertices = [self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)] facet = ngon(len(vertices)) if facet: - meshes.append(conversions.vertices_and_faces_to_rhino(vertices, [facet])) + meshes.append(conversions.vertices_and_faces_to_rhino(vertices, [facet], color=color)) return meshes - def draw_cells(self, cells=None, color=None): + def draw_cells(self): """Draw a selection of cells. - Parameters - ---------- - cells : list[int], optional - A list of cells to draw. - 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. - Returns ------- list[:rhino:`Rhino.Geometry.Mesh`] """ - self.cellcolor = color - meshes = [] - for cell in cells or self.volmesh.cells(): # type: ignore - color = self.cellcolor[cell] # type: ignore + cells = list(self.volmesh.cells()) if self.show_cells is True else self.show_cells or [] + + for cell in cells: + color = self.cellcolor[cell] - vertices = self.volmesh.cell_vertices(cell) # type: ignore - faces = self.volmesh.cell_faces(cell) # type: ignore + vertices = self.volmesh.cell_vertices(cell) + faces = self.volmesh.cell_faces(cell) 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 + faces = [[vertex_index[vertex] for vertex in self.volmesh.halfface_vertices(face)] for face in faces] - mesh = conversions.vertices_and_faces_to_rhino(vertices, faces, disjoint=True) + mesh = conversions.vertices_and_faces_to_rhino(vertices, faces, disjoint=True, color=color) meshes.append(mesh) return meshes diff --git a/src/compas_ghpython/utilities/__init__.py b/src/compas_ghpython/utilities/__init__.py index d52c4f7e5c9..4a816968760 100644 --- a/src/compas_ghpython/utilities/__init__.py +++ b/src/compas_ghpython/utilities/__init__.py @@ -1,6 +1,7 @@ from __future__ import absolute_import +from warnings import warn -from .drawing import ( +from ..drawing import ( draw_frame, draw_points, draw_lines, @@ -31,3 +32,5 @@ "draw_circles", "draw_brep", ] + +warn("compas_ghpython.utilities will be removed in version 2.3. Please use compas_ghpython.drawing instead.", DeprecationWarning, stacklevel=2) diff --git a/src/compas_rhino/utilities/drawing.py b/src/compas_rhino/drawing.py similarity index 100% rename from src/compas_rhino/utilities/drawing.py rename to src/compas_rhino/drawing.py diff --git a/src/compas_rhino/scene/boxobject.py b/src/compas_rhino/scene/boxobject.py index b084630bfd1..6ef643fb3b9 100644 --- a/src/compas_rhino/scene/boxobject.py +++ b/src/compas_rhino/scene/boxobject.py @@ -12,20 +12,7 @@ class RhinoBoxObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing box shapes. - - Parameters - ---------- - box : :class:`compas.geometry.Box` - A COMPAS box. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, box, **kwargs): - super(RhinoBoxObject, self).__init__(geometry=box, **kwargs) - self.box = box + """Scene object for drawing box shapes.""" def draw(self): """Draw the box associated with the scene object. diff --git a/src/compas_rhino/scene/brepobject.py b/src/compas_rhino/scene/brepobject.py index 24c4bbe9186..7e9bed8a3e4 100644 --- a/src/compas_rhino/scene/brepobject.py +++ b/src/compas_rhino/scene/brepobject.py @@ -12,17 +12,7 @@ class RhinoBrepObject(RhinoSceneObject, GeometryObject): - """A scene object for drawing a RhinoBrep. - - Parameters - ---------- - brep : :class:`compas_rhino.geometry.RhinoBrep` - The Brep to draw. - - """ - - def __init__(self, brep, **kwargs): - super(RhinoBrepObject, self).__init__(geometry=brep, **kwargs) + """A scene object for drawing a RhinoBrep.""" def draw(self): """Bakes the Brep into the current document diff --git a/src/compas_rhino/scene/capsuleobject.py b/src/compas_rhino/scene/capsuleobject.py index 52e59dc95ee..9774a034ab2 100644 --- a/src/compas_rhino/scene/capsuleobject.py +++ b/src/compas_rhino/scene/capsuleobject.py @@ -12,19 +12,7 @@ class RhinoCapsuleObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing capsule shapes. - - Parameters - ---------- - capsule : :class:`compas.geometry.Capsule` - A COMPAS capsule. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, capsule, **kwargs): - super(RhinoCapsuleObject, self).__init__(geometry=capsule, **kwargs) + """Scene object for drawing capsule shapes.""" def draw(self): """Draw the capsule associated with the scene object. diff --git a/src/compas_rhino/scene/circleobject.py b/src/compas_rhino/scene/circleobject.py index 12067e24e45..e2a23f176e1 100644 --- a/src/compas_rhino/scene/circleobject.py +++ b/src/compas_rhino/scene/circleobject.py @@ -12,19 +12,7 @@ class RhinoCircleObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing circles. - - Parameters - ---------- - circle : :class:`compas.geometry.Circle` - A COMPAS circle. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, circle, **kwargs): - super(RhinoCircleObject, self).__init__(geometry=circle, **kwargs) + """Scene object for drawing circles.""" def draw(self): """Draw the circle. diff --git a/src/compas_rhino/scene/coneobject.py b/src/compas_rhino/scene/coneobject.py index 2f578acff58..53bbe9c10ec 100644 --- a/src/compas_rhino/scene/coneobject.py +++ b/src/compas_rhino/scene/coneobject.py @@ -12,19 +12,7 @@ class RhinoConeObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing cone shapes. - - Parameters - ---------- - shape : :class:`compas.geometry.Cone` - A COMPAS cone. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, cone, **kwargs): - super(RhinoConeObject, self).__init__(geometry=cone, **kwargs) + """Scene object for drawing cone shapes.""" def draw(self): """Draw the cone associated with the scene object. diff --git a/src/compas_rhino/scene/curveobject.py b/src/compas_rhino/scene/curveobject.py index a990c6f0cec..bffb887367d 100644 --- a/src/compas_rhino/scene/curveobject.py +++ b/src/compas_rhino/scene/curveobject.py @@ -12,19 +12,7 @@ class RhinoCurveObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing curves. - - Parameters - ---------- - curve : :class:`compas.geometry.Curve` - A COMPAS curve. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, curve, **kwargs): - super(RhinoCurveObject, self).__init__(geometry=curve, **kwargs) + """Scene object for drawing curves.""" def draw(self): """Draw the curve. diff --git a/src/compas_rhino/scene/cylinderobject.py b/src/compas_rhino/scene/cylinderobject.py index d09115c38f7..b29060f45ce 100644 --- a/src/compas_rhino/scene/cylinderobject.py +++ b/src/compas_rhino/scene/cylinderobject.py @@ -12,19 +12,7 @@ class RhinoCylinderObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing cylinder shapes. - - Parameters - ---------- - cylinder : :class:`compas.geometry.Cylinder` - A COMPAS cylinder. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, cylinder, **kwargs): - super(RhinoCylinderObject, self).__init__(geometry=cylinder, **kwargs) + """Scene object for drawing cylinder shapes.""" def draw(self): """Draw the cylinder associated with the scene object. diff --git a/src/compas_rhino/scene/ellipseobject.py b/src/compas_rhino/scene/ellipseobject.py index 154d4ffe9ec..b0d44c65f01 100644 --- a/src/compas_rhino/scene/ellipseobject.py +++ b/src/compas_rhino/scene/ellipseobject.py @@ -12,19 +12,7 @@ class RhinoEllipseObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing ellipses. - - Parameters - ---------- - ellipse : :class:`compas.geometry.Ellipse` - A COMPAS ellipse. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, ellipse, **kwargs): - super(RhinoEllipseObject, self).__init__(geometry=ellipse, **kwargs) + """Scene object for drawing ellipses.""" def draw(self): """Draw the ellipse. diff --git a/src/compas_rhino/scene/frameobject.py b/src/compas_rhino/scene/frameobject.py index 17168dc2eda..ae7d3c4beb4 100644 --- a/src/compas_rhino/scene/frameobject.py +++ b/src/compas_rhino/scene/frameobject.py @@ -17,8 +17,6 @@ class RhinoFrameObject(RhinoSceneObject, GeometryObject): Parameters ---------- - frame: :class:`compas.geometry.Frame` - A COMPAS frame. scale: float, optional Scale factor that controls the length of the axes. **kwargs : dict, optional @@ -40,8 +38,8 @@ class RhinoFrameObject(RhinoSceneObject, GeometryObject): """ - def __init__(self, frame, scale=1.0, **kwargs): - super(RhinoFrameObject, self).__init__(geometry=frame, **kwargs) + def __init__(self, scale=1.0, **kwargs): + super(RhinoFrameObject, self).__init__(**kwargs) self.scale = scale or 1.0 self.color_origin = Color.black() self.color_xaxis = Color.red() diff --git a/src/compas_rhino/scene/graphobject.py b/src/compas_rhino/scene/graphobject.py index b9a043b5dbc..88f1e409966 100644 --- a/src/compas_rhino/scene/graphobject.py +++ b/src/compas_rhino/scene/graphobject.py @@ -6,14 +6,10 @@ import scriptcontext as sc # type: ignore import compas_rhino -from compas.geometry import Cylinder from compas.geometry import Line -from compas.geometry import Sphere from compas.scene import GraphObject -from compas_rhino.conversions import cylinder_to_rhino_brep from compas_rhino.conversions import line_to_rhino from compas_rhino.conversions import point_to_rhino -from compas_rhino.conversions import sphere_to_rhino from .sceneobject import RhinoSceneObject @@ -23,21 +19,28 @@ class RhinoGraphObject(RhinoSceneObject, GraphObject): Parameters ---------- - graph : :class:`compas.datastructures.Graph` - A COMPAS graph. + nodegroup : str, optional + The name of the group for the nodes. + edgegroup : str, optional + The name of the group for the edges. + edgedirection : bool, optional + Flag for drawing the edges with an arrow indicating the direction. **kwargs : dict, optional Additional keyword arguments. """ - def __init__(self, graph, **kwargs): - super(RhinoGraphObject, self).__init__(graph=graph, **kwargs) + def __init__(self, nodegroup=None, edgegroup=None, edgedirection=False, **kwargs): + super(RhinoGraphObject, self).__init__(**kwargs) self._guids_nodes = None self._guids_edges = None self._guids_nodelabels = None self._guids_edgelabels = None self._guids_spheres = None self._guids_pipes = None + self.nodegroup = nodegroup + self.edgegroup = edgegroup + self.edgedirection = edgedirection # ========================================================================== # clear @@ -86,23 +89,15 @@ def draw(self): The GUIDs of the created Rhino objects. """ - self.clear() - guids = self.draw_nodes(nodes=self.show_nodes, color=self.nodecolor, group=self.group) - guids += self.draw_edges(edges=self.show_edges, color=self.edgecolor, group=self.group) + guids = self.draw_nodes() + guids += self.draw_edges() + self._guids = guids return self.guids - def draw_nodes(self, nodes=None, color=None, group=None): + def draw_nodes(self): """Draw a selection of nodes. - Parameters - ---------- - nodes : list[int], optional - A list of nodes to draw. - 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. - Returns ------- list[System.Guid] @@ -111,41 +106,29 @@ def draw_nodes(self, nodes=None, color=None, group=None): """ guids = [] - self.nodecolor = color + nodes = list(self.graph.nodes()) if self.show_nodes is True else self.show_nodes or [] - if nodes is True: - nodes = list(self.graph.nodes()) + if nodes: + for node in nodes: + name = "{}.node.{}".format(self.graph.name, node) + attr = self.compile_attributes(name=name, color=self.nodecolor[node]) + geometry = point_to_rhino(self.node_xyz[node]) - for node in nodes or self.graph.nodes(): # type: ignore - name = "{}.node.{}".format(self.graph.name, node) # type: ignore - attr = self.compile_attributes(name=name, color=self.nodecolor[node]) - geometry = point_to_rhino(self.node_xyz[node]) - - guid = sc.doc.Objects.AddPoint(geometry, attr) - guids.append(guid) + guid = sc.doc.Objects.AddPoint(geometry, attr) + guids.append(guid) - if group: - self.add_to_group(group, guids) + if guids: + if self.nodegroup: + self.add_to_group(self.nodegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) self._guids_nodes = guids - return guids - def draw_edges(self, edges=None, color=None, group=None, show_direction=False): + def draw_edges(self): """Draw a selection of edges. - Parameters - ---------- - 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 - Color of the edges. - group : str, optional - The name of a group to add the edges to. - show_direction : bool, optional - Show the direction of the edges. - Returns ------- list[System.Guid] @@ -154,28 +137,28 @@ def draw_edges(self, edges=None, color=None, group=None, show_direction=False): """ guids = [] - arrow = "end" if show_direction else None - self.edgecolor = color + edges = list(self.graph.edges()) if self.show_edges is True else self.show_edges or [] + edgedirection = "end" if self.edgedirection else False - if edges is True: - edges = list(self.graph.edges()) + if edges: + for edge in edges: + u, v = edge - for edge in edges or self.graph.edges(): # type: ignore - u, v = edge - - color = self.edgecolor[edge] - name = "{}.edge.{}-{}".format(self.graph.name, u, v) # type: ignore - attr = self.compile_attributes(name=name, color=color, arrow=arrow) - geometry = line_to_rhino((self.node_xyz[u], self.node_xyz[v])) + color = self.edgecolor[edge] + name = "{}.edge.{}-{}".format(self.graph.name, u, v) + attr = self.compile_attributes(name=name, color=color, arrow=edgedirection) + geometry = line_to_rhino((self.node_xyz[u], self.node_xyz[v])) - guid = sc.doc.Objects.AddLine(geometry, attr) - guids.append(guid) + guid = sc.doc.Objects.AddLine(geometry, attr) + guids.append(guid) - if group: - self.add_to_group(group, guids) + if guids: + if self.edgegroup: + self.add_to_group(self.edgegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) self._guids_edges = guids - return guids # ============================================================================= @@ -277,88 +260,3 @@ def draw_edgelabels(self, text, color=None, group=None, fontheight=10, fontface= self._guids_edgelabels = guids return guids - - # ============================================================================= - # draw miscellaneous - # ============================================================================= - - def draw_spheres(self, radius, color=None, group=None): - """Draw spheres at the vertices of the graph. - - 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.nodecolor = color - - for node in radius: - name = "{}.node.{}.sphere".format(self.graph.name, node) # type: ignore - color = self.nodecolor[node] - attr = self.compile_attributes(name=name, color=color) - - 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) - - self._guids_spheres = guids - - return guids - - def draw_pipes(self, radius, color=None, group=None): - """Draw pipes around the edges of the graph. - - 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.edgecolor = color - - for edge in radius: - name = "{}.edge.{}-{}.pipe".format(self.graph.name, *edge) # type: ignore - color = self.edgecolor[edge] - attr = self.compile_attributes(name=name, color=color) - - 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) - - self._guids_pipes = guids - - return guids diff --git a/src/compas_rhino/scene/lineobject.py b/src/compas_rhino/scene/lineobject.py index 420cb2b71cc..9ab5c22e840 100644 --- a/src/compas_rhino/scene/lineobject.py +++ b/src/compas_rhino/scene/lineobject.py @@ -12,19 +12,7 @@ class RhinoLineObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing lines. - - Parameters - ---------- - line : :class:`compas.geometry.Line` - A COMPAS line. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, line, **kwargs): - super(RhinoLineObject, self).__init__(geometry=line, **kwargs) + """Scene object for drawing lines.""" def draw(self): """Draw the line. diff --git a/src/compas_rhino/scene/meshobject.py b/src/compas_rhino/scene/meshobject.py index 799eca74ed3..db04e6c9eb2 100644 --- a/src/compas_rhino/scene/meshobject.py +++ b/src/compas_rhino/scene/meshobject.py @@ -7,18 +7,14 @@ import compas_rhino.objects from compas.colors import Color -from compas.geometry import Cylinder from compas.geometry import Line from compas.geometry import Point -from compas.geometry import Sphere from compas.geometry import centroid_points from compas.scene import MeshObject -from compas_rhino.conversions import cylinder_to_rhino_brep from compas_rhino.conversions import line_to_rhino -from compas_rhino.conversions import mesh_to_rhino from compas_rhino.conversions import point_to_rhino -from compas_rhino.conversions import sphere_to_rhino -from compas_rhino.conversions import transformation_to_rhino + +# from compas_rhino.conversions import transformation_to_rhino from compas_rhino.conversions import vertices_and_faces_to_rhino from .helpers import ngon @@ -30,8 +26,15 @@ class RhinoMeshObject(RhinoSceneObject, MeshObject): Parameters ---------- - mesh : :class:`compas.datastructures.Mesh` - A COMPAS mesh. + disjoint : bool, optional + Draw the faces of the mesh disjointed. + Default is ``False``. + vertexgroup : str, optional + The name of the group for the vertices. + edgegroup : str, optional + The name of the group for the edges. + facegroup : str, optional + The name of the group for the faces. **kwargs : dict, optional Additional keyword arguments. @@ -40,11 +43,17 @@ class RhinoMeshObject(RhinoSceneObject, MeshObject): disjoint : bool, optional Draw the faces of the mesh disjointed. Default is ``False``. + vertexgroup : str, optional + The name of the group for the vertices. + edgegroup : str, optional + The name of the group for the edges. + facegroup : str, optional + The name of the group for the faces. """ - def __init__(self, mesh, disjoint=False, **kwargs): - super(RhinoMeshObject, self).__init__(mesh=mesh, **kwargs) + def __init__(self, disjoint=False, vertexgroup=None, edgegroup=None, facegroup=None, **kwargs): + super(RhinoMeshObject, self).__init__(**kwargs) self.disjoint = disjoint self._guid_mesh = None self._guids_faces = None @@ -57,6 +66,9 @@ def __init__(self, mesh, disjoint=False, **kwargs): self._guids_facelabels = None self._guids_spheres = None self._guids_pipes = None + self.vertexgroup = vertexgroup + self.edgegroup = edgegroup + self.facegroup = facegroup # ========================================================================== # clear @@ -188,15 +200,22 @@ def draw(self): if len(self.facecolor): facecolors = [self.facecolor[face] for face in self.mesh.faces()] - geometry = mesh_to_rhino( - self.mesh, + 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()] + + geometry = vertices_and_faces_to_rhino( + vertices, + faces, color=self.color, vertexcolors=vertexcolors, facecolors=facecolors, disjoint=self.disjoint, ) - geometry.Transform(transformation_to_rhino(self.worldtransformation)) + # geometry.Transform(transformation_to_rhino(self.worldtransformation)) self._guid_mesh = sc.doc.Objects.AddMesh(geometry, attr) if self.group: @@ -205,29 +224,19 @@ def draw(self): self._guids.append(self._guid_mesh) elif self.show_faces: - self._guids += self.draw_faces(faces=self.show_faces, color=self.facecolor, group=self.group) + self._guids += self.draw_faces() if self.show_vertices: - self._guids += self.draw_vertices(vertices=self.show_vertices, color=self.vertexcolor, group=self.group) + self._guids += self.draw_vertices() if self.show_edges: - self._guids += self.draw_edges(edges=self.show_edges, color=self.edgecolor, group=self.group) + self._guids += self.draw_edges() return self.guids - def draw_vertices(self, vertices=None, color=None, group=None): + def draw_vertices(self): """Draw a selection of vertices. - Parameters - ---------- - 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 of the vertices. - group : str, optional - The name of a group to join the created Rhino objects in. - Returns ------- list[System.Guid] @@ -236,45 +245,31 @@ def draw_vertices(self, vertices=None, color=None, group=None): """ guids = [] - self.vertexcolor = color + vertices = list(self.mesh.vertices()) if self.show_vertices is True else self.show_vertices or [] - if vertices is True: - vertices = list(self.mesh.vertices()) + if vertices: + for vertex in vertices: + name = "{}.vertex.{}".format(self.mesh.name, vertex) + color = self.vertexcolor[vertex] + attr = self.compile_attributes(name=name, color=color) - for vertex in vertices or self.mesh.vertices(): # type: ignore - name = "{}.vertex.{}".format(self.mesh.name, vertex) # type: ignore - color = self.vertexcolor[vertex] - attr = self.compile_attributes(name=name, color=color) - - point = point_to_rhino(self.vertex_xyz[vertex]) + point = point_to_rhino(self.vertex_xyz[vertex]) - guid = sc.doc.Objects.AddPoint(point, attr) - guids.append(guid) + guid = sc.doc.Objects.AddPoint(point, attr) + guids.append(guid) - if group: - self.add_to_group(group, guids) + if guids: + if self.vertexgroup: + self.add_to_group(self.vertexgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) self._guids_vertices = guids - return guids - def draw_edges(self, edges=None, color=None, text=None, fontheight=10, fontface="Arial Regular", group=None): + def draw_edges(self): """Draw a selection of edges. - Parameters - ---------- - 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 of the edges. - 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] @@ -283,45 +278,31 @@ def draw_edges(self, edges=None, color=None, text=None, fontheight=10, fontface= """ guids = [] - self.edgecolor = color - - if edges is True: - edges = list(self.mesh.edges()) + edges = list(self.mesh.edges()) if self.show_edges is True else self.show_edges or [] - for edge in edges or self.mesh.edges(): # type: ignore - name = "{}.edge.{}-{}".format(self.mesh.name, *edge) # type: ignore - color = self.edgecolor[edge] - attr = self.compile_attributes(name=name, color=color) + if edges: + for edge in edges: + name = "{}.edge.{}-{}".format(self.mesh.name, *edge) + color = self.edgecolor[edge] + attr = self.compile_attributes(name=name, color=color) - line = Line(self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]) + 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) + guid = sc.doc.Objects.AddLine(line_to_rhino(line), attr) + guids.append(guid) - if group: - self.add_to_group(group, guids) + if guids: + if self.edgegroup: + self.add_to_group(self.edgegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) self._guids_edges = guids - return guids - def draw_faces(self, faces=None, color=None, group=None): + def draw_faces(self): """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 of the faces. - 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] @@ -330,28 +311,28 @@ def draw_faces(self, faces=None, color=None, group=None): """ guids = [] - self.facecolor = color + faces = list(self.mesh.faces()) if self.show_faces is True else self.show_faces or [] - if faces is True: - faces = list(self.mesh.faces()) + if faces: + for face in faces: + name = "{}.face.{}".format(self.mesh.name, face) + color = self.facecolor[face] + attr = self.compile_attributes(name=name, color=color) - for face in faces or self.mesh.faces(): # type: ignore - name = "{}.face.{}".format(self.mesh.name, face) # type: ignore - color = self.facecolor[face] - attr = self.compile_attributes(name=name, color=color) + vertices = [self.vertex_xyz[vertex] for vertex in self.mesh.face_vertices(face)] # type: ignore + facet = ngon(len(vertices)) - 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 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) + if guids: + if self.facegroup: + self.add_to_group(self.facegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) self._guids_faces = guids - return guids # ========================================================================== @@ -586,88 +567,3 @@ def draw_facenormals(self, faces=None, color=(0, 255, 255), scale=1.0, group=Non self._guids_facenormals = guids return guids - - # ========================================================================== - # draw miscellaneous - # ========================================================================== - - def draw_spheres(self, radius, color=None, group=None): - """Draw spheres at the vertices of the mesh. - - Parameters - ---------- - 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 - ------- - list[System.Guid] - The GUIDs of the created Rhino objects. - - """ - guids = [] - - self.vertexcolor = color - - for vertex in radius: - name = "{}.vertex.{}.sphere".format(self.mesh.name, vertex) # type: ignore - color = self.vertexcolor[vertex] - attr = self.compile_attributes(name=name, color=color) - - 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) - - self._guids_spheres = guids - - return guids - - def draw_pipes(self, radius, color=None, group=None): - """Draw pipes around the edges of the mesh. - - Parameters - ---------- - 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 - ------- - list[System.Guid] - The GUIDs of the created Rhino objects. - - """ - guids = [] - - self.edgecolor = color - - for edge in radius: - name = "{}.edge.{}-{}.pipe".format(self.mesh.name, *edge) # type: ignore - color = self.edgecolor[edge] - attr = self.compile_attributes(name=name, color=color) - - 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) - - self._guids_pipes = guids - - return guids diff --git a/src/compas_rhino/scene/planeobject.py b/src/compas_rhino/scene/planeobject.py index 18b88259f10..eece052c32c 100644 --- a/src/compas_rhino/scene/planeobject.py +++ b/src/compas_rhino/scene/planeobject.py @@ -18,18 +18,22 @@ class RhinoPlaneObject(RhinoSceneObject, GeometryObject): Parameters ---------- - plane : :class:`compas.geometry.Plane` - A COMPAS plane. scale : float, optional Scale factor. Default is ``1.0``. **kwargs : dict, optional Additional keyword arguments. + Attributes + ---------- + scale : float + Scale factor. + Default is ``1.0``. + """ - def __init__(self, plane, scale=1.0, **kwargs): - super(RhinoPlaneObject, self).__init__(geometry=plane, **kwargs) + def __init__(self, scale=1.0, **kwargs): + super(RhinoPlaneObject, self).__init__(**kwargs) self.scale = scale def draw(self): diff --git a/src/compas_rhino/scene/pointobject.py b/src/compas_rhino/scene/pointobject.py index 0ee1014810c..4091fb7534b 100644 --- a/src/compas_rhino/scene/pointobject.py +++ b/src/compas_rhino/scene/pointobject.py @@ -12,19 +12,7 @@ class RhinoPointObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing points. - - Parameters - ---------- - point : :class:`compas.geometry.Point` - A COMPAS point. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, point, **kwargs): - super(RhinoPointObject, self).__init__(geometry=point, **kwargs) + """Scene object for drawing points.""" def draw(self): """Draw the point. diff --git a/src/compas_rhino/scene/polygonobject.py b/src/compas_rhino/scene/polygonobject.py index 35bc52773f0..3cde33d5874 100644 --- a/src/compas_rhino/scene/polygonobject.py +++ b/src/compas_rhino/scene/polygonobject.py @@ -12,19 +12,7 @@ class RhinoPolygonObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing polygons. - - Parameters - ---------- - polygon : :class:`compas.geometry.Polygon` - A COMPAS polygon. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polygon, **kwargs): - super(RhinoPolygonObject, self).__init__(geometry=polygon, **kwargs) + """Scene object for drawing polygons.""" def draw(self): """Draw the polygon. diff --git a/src/compas_rhino/scene/polyhedronobject.py b/src/compas_rhino/scene/polyhedronobject.py index ca6df3505d6..8800026b519 100644 --- a/src/compas_rhino/scene/polyhedronobject.py +++ b/src/compas_rhino/scene/polyhedronobject.py @@ -12,19 +12,7 @@ class RhinoPolyhedronObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing polyhedron shapes. - - Parameters - ---------- - polyhedron : :class:`compas.geometry.Polyhedron` - A COMPAS polyhedron. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polyhedron, **kwargs): - super(RhinoPolyhedronObject, self).__init__(geometry=polyhedron, **kwargs) + """Scene object for drawing polyhedron shapes.""" def draw(self): """Draw the polyhedron associated with the scene object. diff --git a/src/compas_rhino/scene/polylineobject.py b/src/compas_rhino/scene/polylineobject.py index ed98970b958..b00f7226708 100644 --- a/src/compas_rhino/scene/polylineobject.py +++ b/src/compas_rhino/scene/polylineobject.py @@ -12,19 +12,7 @@ class RhinoPolylineObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing polylines. - - Parameters - ---------- - polyline : :class:`compas.geometry.Polyline` - A COMPAS polyline. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, polyline, **kwargs): - super(RhinoPolylineObject, self).__init__(geometry=polyline, **kwargs) + """Scene object for drawing polylines.""" def draw(self): """Draw the polyline. diff --git a/src/compas_rhino/scene/sceneobject.py b/src/compas_rhino/scene/sceneobject.py index 2c2ecfcfadc..ac1f098b655 100644 --- a/src/compas_rhino/scene/sceneobject.py +++ b/src/compas_rhino/scene/sceneobject.py @@ -25,6 +25,13 @@ class RhinoSceneObject(SceneObject): **kwargs : dict, optional Additional keyword arguments. + Attributes + ---------- + layer : str + The name of the layer. + group : str + The name of the group. + """ def __init__(self, layer=None, group=None, **kwargs): diff --git a/src/compas_rhino/scene/sphereobject.py b/src/compas_rhino/scene/sphereobject.py index 3d68d04ae72..ad6278cff99 100644 --- a/src/compas_rhino/scene/sphereobject.py +++ b/src/compas_rhino/scene/sphereobject.py @@ -12,19 +12,7 @@ class RhinoSphereObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing sphere shapes. - - Parameters - ---------- - sphere : :class:`compas.geometry.Sphere` - A COMPAS sphere. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, sphere, **kwargs): - super(RhinoSphereObject, self).__init__(geometry=sphere, **kwargs) + """Scene object for drawing sphere shapes.""" def draw(self): """Draw the sphere associated with the scene object. diff --git a/src/compas_rhino/scene/surfaceobject.py b/src/compas_rhino/scene/surfaceobject.py index 7edfd3c7d0f..e352b237584 100644 --- a/src/compas_rhino/scene/surfaceobject.py +++ b/src/compas_rhino/scene/surfaceobject.py @@ -12,19 +12,7 @@ class RhinoSurfaceObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing surfaces. - - Parameters - ---------- - surface : :class:`compas.geometry.Geometry` - A COMPAS surface. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, surface, **kwargs): - super(RhinoSurfaceObject, self).__init__(geometry=surface, **kwargs) + """Scene object for drawing surfaces.""" def draw(self): """Draw the surface. diff --git a/src/compas_rhino/scene/torusobject.py b/src/compas_rhino/scene/torusobject.py index 4fe21f9d18f..477d24403dc 100644 --- a/src/compas_rhino/scene/torusobject.py +++ b/src/compas_rhino/scene/torusobject.py @@ -12,19 +12,7 @@ class RhinoTorusObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing torus shapes. - - Parameters - ---------- - torus : :class:`compas.geometry.Torus` - A COMPAS torus. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, torus, **kwargs): - super(RhinoTorusObject, self).__init__(geometry=torus, **kwargs) + """Scene object for drawing torus shapes.""" def draw(self): """Draw the torus associated with the scene object. diff --git a/src/compas_rhino/scene/vectorobject.py b/src/compas_rhino/scene/vectorobject.py index 3c0f39739c0..d95eb321a66 100644 --- a/src/compas_rhino/scene/vectorobject.py +++ b/src/compas_rhino/scene/vectorobject.py @@ -13,19 +13,7 @@ class RhinoVectorObject(RhinoSceneObject, GeometryObject): - """Scene object for drawing vectors. - - Parameters - ---------- - vector : :class:`compas.geometry.Vector` - A COMPAS vector. - **kwargs : dict, optional - Additional keyword arguments. - - """ - - def __init__(self, vector, **kwargs): - super(RhinoVectorObject, self).__init__(geometry=vector, **kwargs) + """Scene object for drawing vectors.""" def draw(self): """Draw the vector. diff --git a/src/compas_rhino/scene/volmeshobject.py b/src/compas_rhino/scene/volmeshobject.py index 3be88d5312c..9a512a1c82f 100644 --- a/src/compas_rhino/scene/volmeshobject.py +++ b/src/compas_rhino/scene/volmeshobject.py @@ -22,18 +22,38 @@ class RhinoVolMeshObject(RhinoSceneObject, VolMeshObject): Parameters ---------- - volmesh : :class:`compas.datastructures.VolMesh` - A COMPAS volmesh. disjoint : bool, optional Draw the faces of the mesh disjointed. Default is ``True``. + vertexgroup : str, optional + The name of the group for the vertices. + edgegroup : str, optional + The name of the group for the edges. + facegroup : str, optional + The name of the group for the faces. + cellgroup : str, optional + The name of the group for the cells. **kwargs : dict, optional Additional keyword arguments. + Attributes + ---------- + disjoint : bool + Draw the faces of the mesh disjointed. + Default is ``True``. + vertexgroup : str + The name of the group for the vertices. + edgegroup : str + The name of the group for the edges. + facegroup : str + The name of the group for the faces. + cellgroup : str + The name of the group for the cells. + """ - def __init__(self, volmesh, disjoint=True, **kwargs): - super(RhinoVolMeshObject, self).__init__(volmesh=volmesh, **kwargs) + def __init__(self, disjoint=True, vertexgroup=None, edgegroup=None, facegroup=None, cellgroup=None, **kwargs): + super(RhinoVolMeshObject, self).__init__(**kwargs) self.disjoint = disjoint self._guids_vertices = None self._guids_edges = None @@ -43,6 +63,10 @@ def __init__(self, volmesh, disjoint=True, **kwargs): self._guids_edgelabels = None self._guids_facelabels = None self._guids_celllabels = None + self.vertexgroup = vertexgroup + self.edgegroup = edgegroup + self.facegroup = facegroup + self.cellgroup = cellgroup # ========================================================================== # clear @@ -145,153 +169,122 @@ def draw(self): guids = [] if self.show_vertices: - guids += self.draw_vertices(vertices=self.show_vertices, color=self.vertexcolor, group=self.group) + guids += self.draw_vertices() if self.show_edges: - guids += self.draw_edges(edges=self.show_edges, color=self.edgecolor, group=self.group) + guids += self.draw_edges() if self.show_faces: - guids += self.draw_faces(faces=self.show_faces, color=self.facecolor, group=self.group) + guids += self.draw_faces() if self.show_cells: - guids += self.draw_cells(cells=self.show_cells, color=self.cellcolor, group=self.group) + guids += self.draw_cells() self._guids = guids return self.guids - def draw_vertices(self, vertices=None, color=None, group=None): + def draw_vertices(self): """Draw a selection of vertices. - Parameters - ---------- - vertices : list[int], optional - 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`], optional - The color of the vertices. - group : str, optional - The name of the group in which the vertices are combined. - Returns ------- list[System.Guid] The GUIDs of the created Rhino point objects. """ - self.vertexcolor = color - guids = [] - if vertices is True: - vertices = list(self.volmesh.vertices()) + vertices = list(self.volmesh.vertices()) if self.show_vertices is True else self.show_vertices or [] - for vertex in vertices or self.volmesh.vertices(): # type: ignore - name = "{}.vertex.{}".format(self.volmesh.name, vertex) # type: ignore - color = self.vertexcolor[vertex] - attr = self.compile_attributes(name=name, color=color) + if vertices: + for vertex in vertices: + name = "{}.vertex.{}".format(self.volmesh.name, vertex) + color = self.vertexcolor[vertex] + attr = self.compile_attributes(name=name, color=color) - point = self.vertex_xyz[vertex] + point = self.vertex_xyz[vertex] - guid = sc.doc.Objects.AddPoint(point_to_rhino(point), attr) - guids.append(guid) + guid = sc.doc.Objects.AddPoint(point_to_rhino(point), attr) + guids.append(guid) + if guids: + if self.vertexgroup: + self.add_to_group(self.vertexgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + + self._guids_vertices = guids return guids - def draw_edges(self, edges=None, color=None, group=None): + def draw_edges(self): """Draw a selection of edges. - Parameters - ---------- - 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 of the edges. - group : str, optional - The name of the group in which the edges are combined. - Returns ------- list[System.Guid] The GUIDs of the created Rhino line objects. """ - self.edgecolor = color - guids = [] - if edges is True: - edges = list(self.volmesh.edges()) + edges = list(self.volmesh.edges()) if self.show_edges is True else self.show_edges or [] - for edge in edges or self.volmesh.edges(): # type: ignore - name = "{}.edge.{}-{}".format(self.volmesh.name, *edge) # type: ignore - color = self.edgecolor[edge] - attr = self.compile_attributes(name=name, color=color) + if edges: + for edge in edges: + name = "{}.edge.{}-{}".format(self.volmesh.name, *edge) + color = self.edgecolor[edge] + attr = self.compile_attributes(name=name, color=color) - line = Line(self.vertex_xyz[edge[0]], self.vertex_xyz[edge[1]]) + 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) + guid = sc.doc.Objects.AddLine(line_to_rhino(line), attr) + guids.append(guid) - if group: - self.add_to_group(group, guids) + if guids: + if self.edgegroup: + self.add_to_group(self.edgegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + self._guids_edges = guids return guids - def draw_faces(self, faces=None, color=None, group=None): + def draw_faces(self): """Draw a selection of faces. - Parameters - ---------- - faces : list[int], optional - A list 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 of the faces. - group : str, optional - The name of the group in which the faces are combined. - Returns ------- list[System.Guid] The GUIDs of the created Rhino objects. """ - self.facecolor = color - guids = [] - if faces is True: - faces = list(self.volmesh.faces()) + faces = list(self.volmesh.faces()) if self.show_faces is True else self.show_faces or [] - for face in faces or self.volmesh.faces(): # type: ignore - name = "{}.face.{}".format(self.volmesh.name, face) # type: ignore - color = self.facecolor[face] - attr = self.compile_attributes(name=name, color=color) + if faces: + for face in faces: + name = "{}.face.{}".format(self.volmesh.name, face) + color = self.facecolor[face] + attr = self.compile_attributes(name=name, color=color) - vertices = [self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)] # type: ignore - facet = ngon(len(vertices)) + vertices = [self.vertex_xyz[vertex] for vertex in self.volmesh.face_vertices(face)] + facet = ngon(len(vertices)) - if facet: - guid = sc.doc.Objects.AddMesh(vertices_and_faces_to_rhino(vertices, [facet]), attr) - guids.append(guid) + 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) + if guids: + if self.facegroup: + self.add_to_group(self.facegroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + self._guids_faces = guids return guids - def draw_cells(self, cells=None, color=None, group=None): + def draw_cells(self): """Draw a selection of cells. - Parameters - ---------- - cells : list[int], optional - A list of cells to draw. - 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. - group : str, optional - The name of the group in which the cells are combined. - Returns ------- list[System.Guid] @@ -299,27 +292,33 @@ def draw_cells(self, cells=None, color=None, group=None): Every cell is drawn as an individual mesh. """ - self.cellcolor = color - guids = [] - if cells is True: - cells = list(self.volmesh.cells()) + cells = list(self.volmesh.cells()) if self.show_cells is True else self.show_cells or [] - for cell in cells or self.volmesh.cells(): # type: ignore - name = "{}.cell.{}".format(self.volmesh.name, cell) # type: ignore - color = self.cellcolor[cell] - attr = self.compile_attributes(name=name, color=color) + if cells: + for cell in cells: + name = "{}.cell.{}".format(self.volmesh.name, cell) + color = self.cellcolor[cell] + attr = self.compile_attributes(name=name, color=color) - 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 + vertices = self.volmesh.cell_vertices(cell) + faces = self.volmesh.cell_faces(cell) + vertex_index = dict((vertex, index) for index, vertex in enumerate(vertices)) - guid = sc.doc.Objects.AddMesh(vertices_and_faces_to_rhino(vertices, faces, disjoint=self.disjoint), attr) - guids.append(guid) + 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] + + guid = sc.doc.Objects.AddMesh(vertices_and_faces_to_rhino(vertices, faces, disjoint=self.disjoint), attr) + guids.append(guid) + + if guids: + if self.cellgroup: + self.add_to_group(self.cellgroup, guids) + elif self.group: + self.add_to_group(self.group, guids) + self._guids_cells = guids return guids # ============================================================================= @@ -508,11 +507,3 @@ def draw_celllabels(self, text, color=None, group=None, fontheight=10, fontface= self.add_to_group(group, guids) return guids - - # ============================================================================= - # draw normals - # ============================================================================= - - # ============================================================================= - # draw miscellaneous - # ============================================================================= diff --git a/src/compas_rhino/utilities/__init__.py b/src/compas_rhino/utilities/__init__.py index 70efdc72bf5..51790bb787e 100644 --- a/src/compas_rhino/utilities/__init__.py +++ b/src/compas_rhino/utilities/__init__.py @@ -1,6 +1,7 @@ from __future__ import absolute_import +from warnings import warn -from .drawing import ( +from ..drawing import ( draw_labels, draw_points, draw_lines, @@ -35,3 +36,5 @@ "draw_surfaces", "draw_brep", ] + +warn("compas_rhino.utilities will be removed in version 2.3. Please use compas_rhino.drawing instead.", DeprecationWarning, stacklevel=2) diff --git a/src/compas_rhino/utilities/constructors.py b/src/compas_rhino/utilities/constructors.py deleted file mode 100644 index aa86af86ba5..00000000000 --- a/src/compas_rhino/utilities/constructors.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import Rhino # type: ignore -import scriptcontext as sc # type: ignore - -from compas.tolerance import TOL - - -def volmesh_from_polysurfaces(cls, guids, precision=None): - """Construct a volumetric mesh from given polysurfaces. - - Parameters - ---------- - cls : :class:`compas.datastructures.VolMesh` - The class of volmesh. - guids : sequence[str | System.Guid] - The *globally unique identifiers* of the polysurfaces. - precision: int, optional - Precision for converting numbers to strings. - Default is :attr:`TOL.precision`. - - Returns - ------- - :class:`compas.datastructures.Volmesh` - The volumetric mesh object. - - Notes - ----- - Essentially, this function does the following: - - * find each of the polysurfaces and check if they have a boundary representation (b-rep) - * convert to b-rep and extract the edge loops - * make a face of each loop by referring to vertices using their geometric keys - * add a cell per brep - * and add the faces of a brep to the cell - * create a volmesh from the found vertices and cells - - """ - gkey_xyz = {} - cells = [] - for guid in guids: - cell = [] - obj = sc.doc.Objects.Find(guid) - if not obj.Geometry.HasBrepForm: - continue - brep = Rhino.Geometry.Brep.TryConvertBrep(obj.Geometry) - for loop in brep.Loops: - curve = loop.To3dCurve() - segments = curve.Explode() - face = [] - sp = segments[0].PointAtStart - ep = segments[0].PointAtEnd - sp_gkey = TOL.geometric_key(sp, precision) - ep_gkey = TOL.geometric_key(ep, precision) - gkey_xyz[sp_gkey] = sp - gkey_xyz[ep_gkey] = ep - face.append(sp_gkey) - face.append(ep_gkey) - for segment in segments[1:-1]: - ep = segment.PointAtEnd - ep_gkey = TOL.geometric_key(ep, precision) - face.append(ep_gkey) - gkey_xyz[ep_gkey] = ep - cell.append(face) - cells.append(cell) - gkey_index = dict((gkey, index) for index, gkey in enumerate(gkey_xyz)) - vertices = [list(xyz) for gkey, xyz in gkey_xyz.items()] - cells = [[[gkey_index[gkey] for gkey in face] for face in cell] for cell in cells] - return cls.from_vertices_and_cells(vertices, cells) diff --git a/tests/compas/datastructures/test_cell_network.py b/tests/compas/datastructures/test_cell_network.py index d1fd4b87f6e..dfe62386e6b 100644 --- a/tests/compas/datastructures/test_cell_network.py +++ b/tests/compas/datastructures/test_cell_network.py @@ -80,6 +80,6 @@ def test_cell_network_boundary(example_cell_network): ds = example_cell_network assert set(ds.cells_on_boundaries()) == {0, 1} assert set(ds.faces_on_boundaries()) == {0, 1, 2, 3, 4, 6, 7, 8, 9, 10} - # assert set(ds.faces_no_cell()) == {11} - # assert set(ds.edges_no_face()) == {(13, 15), (12, 14)} - # assert set(ds.non_manifold_edges()) == {(6, 7), (4, 5), (5, 6), (4, 7)} + assert set(ds.faces_without_cell()) == {11} + assert set(ds.edges_without_face()) == {(15, 13), (14, 12)} + assert set(ds.nonmanifold_edges()) == {(6, 7), (4, 5), (5, 6), (7, 4)} diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index 2d2f9764da3..af51e59d355 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -723,6 +723,7 @@ def test_halfedge_before_on_boundary(grid): def test_loops_and_strips_closed(sphere): + # type: (Mesh) -> None poles = list(sphere.vertices_where({"vertex_degree": 16})) for nbr in sphere.vertex_neighbors(poles[0]): diff --git a/tests/compas/datastructures/test_volmesh.py b/tests/compas/datastructures/test_volmesh.py index ac70832effe..d0100d4bcba 100644 --- a/tests/compas/datastructures/test_volmesh.py +++ b/tests/compas/datastructures/test_volmesh.py @@ -27,6 +27,7 @@ def halfface(): def test_halfface_data(halfface): + # type: (VolMesh) -> None other = VolMesh.__from_data__(json.loads(json.dumps(halfface.__data__))) assert halfface.__data__ == other.__data__ @@ -160,7 +161,7 @@ def test_edges_where(): hf.add_vertex(vkey) hf.add_halfface([0, 1, 2]) hf.edge_attribute((0, 1), "a", 5) - assert list(hf.edges_where({"a": 1})) == [(1, 2), (2, 0)] + assert list(hf.edges_where({"a": 1})) == [(0, 2), (1, 2)] def test_edges_where_predicate(): diff --git a/tests/compas/geometry/test_vector.py b/tests/compas/geometry/test_vector.py index ba76c1e42ee..5294775665e 100644 --- a/tests/compas/geometry/test_vector.py +++ b/tests/compas/geometry/test_vector.py @@ -47,11 +47,42 @@ def test_vector2(x, y): def test_vector_operators(): a = Vector(random(), random(), random()) b = Vector(random(), random(), random()) - assert a + b == [a.x + b.x, a.y + b.y, a.z + b.z] - assert a - b == [a.x - b.x, a.y - b.y, a.z - b.z] + c = [random(), random(), random()] + assert a * 2 == [a.x * 2, a.y * 2, a.z * 2] assert a / 2 == [a.x / 2, a.y / 2, a.z / 2] assert a**3 == [a.x**3, a.y**3, a.z**3] + assert 2 * a == [2 * a.x, 2 * a.y, 2 * a.z] + + assert a + b == [a.x + b.x, a.y + b.y, a.z + b.z] + assert a - b == [a.x - b.x, a.y - b.y, a.z - b.z] + assert a * b == [a.x * b.x, a.y * b.y, a.z * b.z] + assert a / b == [a.x / b.x, a.y / b.y, a.z / b.z] + + assert b + a == [a.x + b.x, a.y + b.y, a.z + b.z] + assert b - a == [b.x - a.x, b.y - a.y, b.z - a.z] + assert b * a == [a.x * b.x, a.y * b.y, a.z * b.z] + assert b / a == [b.x / a.x, b.y / a.y, b.z / a.z] + + assert a * c == [a.x * c[0], a.y * c[1], a.z * c[2]] + assert c * a == [a.x * c[0], a.y * c[1], a.z * c[2]] + assert a + c == [a.x + c[0], a.y + c[1], a.z + c[2]] + assert a - c == [a.x - c[0], a.y - c[1], a.z - c[2]] + + assert c * a == [a.x * c[0], a.y * c[1], a.z * c[2]] + assert c / a == [c[0] / a.x, c[1] / a.y, c[2] / a.z] + assert c + a == [a.x + c[0], a.y + c[1], a.z + c[2]] + assert c - a == [c[0] - a.x, c[1] - a.y, c[2] - a.z] + + with pytest.raises(TypeError) as exc_info: + a / "wrong type" + if not compas.IPY: + assert str(exc_info.value) == "Cannot cast wrong type to Vector" + + with pytest.raises(TypeError) as exc_info: + a * "wrong type" + if not compas.IPY: + assert str(exc_info.value) == "Cannot cast wrong type to Vector" def test_vector_equality(): diff --git a/tests/compas/scene/test_scene.py b/tests/compas/scene/test_scene.py index ba1a1c6c565..52da3d72129 100644 --- a/tests/compas/scene/test_scene.py +++ b/tests/compas/scene/test_scene.py @@ -1,14 +1,12 @@ -import pytest # noqa: F401 - import compas -from compas.scene import context -from compas.scene import register -from compas.scene import SceneObject -from compas.scene import SceneObjectNotRegisteredError -from compas.data import Data - if not compas.IPY: + import pytest # noqa: F401 + from compas.scene import context + from compas.scene import register + from compas.scene import SceneObject + from compas.scene import SceneObjectNotRegisteredError + from compas.data import Data @pytest.fixture(autouse=True) def reset_sceneobjects_registration(): @@ -17,73 +15,63 @@ def reset_sceneobjects_registration(): # after each test, reset scene objects context.ITEM_SCENEOBJECT.clear() + def register_fake_context(): + register(FakeItem, FakeSceneObject, context="fake") -def register_fake_context(): - register(FakeItem, FakeSceneObject, context="fake") + class FakeSceneObject(SceneObject): + def draw(self): + pass + def clear(self): + pass -class FakeSceneObject(SceneObject): - def draw(self): - pass - - def clear(self): - pass + class FakeSubSceneObject(SceneObject): + def draw(self): + pass + def clear(self): + pass -class FakeSubSceneObject(SceneObject): - def draw(self): + class FakeItem(Data): pass - def clear(self): + class FakeSubItem(FakeItem): pass + def test_get_sceneobject_cls_with_orderly_registration(): + register(FakeItem, FakeSceneObject, context="fake") + register(FakeSubItem, FakeSubSceneObject, context="fake") + item = FakeItem() + sceneobject = SceneObject(item, context="fake") + assert isinstance(sceneobject, FakeSceneObject) -class FakeItem(Data): - pass - - -class FakeSubItem(FakeItem): - pass - - -def test_get_sceneobject_cls_with_orderly_registration(): - register(FakeItem, FakeSceneObject, context="fake") - register(FakeSubItem, FakeSubSceneObject, context="fake") - item = FakeItem() - sceneobject = SceneObject(item, context="fake") - assert isinstance(sceneobject, FakeSceneObject) - - item = FakeSubItem() - sceneobject = SceneObject(item, context="fake") - assert isinstance(sceneobject, FakeSubSceneObject) - - -def test_get_sceneobject_cls_with_out_of_order_registration(): - register(FakeSubItem, FakeSubSceneObject, context="fake") - register(FakeItem, FakeSceneObject, context="fake") - item = FakeItem() - sceneobject = SceneObject(item, context="fake") - assert isinstance(sceneobject, FakeSceneObject) - - item = FakeSubItem() - sceneobject = SceneObject(item, context="fake") - assert isinstance(sceneobject, FakeSubSceneObject) + item = FakeSubItem() + sceneobject = SceneObject(item, context="fake") + assert isinstance(sceneobject, FakeSubSceneObject) + def test_get_sceneobject_cls_with_out_of_order_registration(): + register(FakeSubItem, FakeSubSceneObject, context="fake") + register(FakeItem, FakeSceneObject, context="fake") + item = FakeItem() + sceneobject = SceneObject(item, context="fake") + assert isinstance(sceneobject, FakeSceneObject) -if not compas.IPY: + item = FakeSubItem() + sceneobject = SceneObject(item, context="fake") + assert isinstance(sceneobject, FakeSubSceneObject) - def test_sceneobject_auto_context_discovery(mocker): - register_fake_context() + def test_sceneobject_auto_context_discovery(mocker): + register_fake_context() - item = FakeItem() - sceneobject = SceneObject(item) + item = FakeItem() + sceneobject = SceneObject(item) - assert isinstance(sceneobject, FakeSceneObject) + assert isinstance(sceneobject, FakeSceneObject) - def test_sceneobject_auto_context_discovery_no_context(mocker): - mocker.patch("compas.scene.context.compas.is_grasshopper", return_value=False) - mocker.patch("compas.scene.context.compas.is_rhino", return_value=False) + def test_sceneobject_auto_context_discovery_no_context(mocker): + mocker.patch("compas.scene.context.compas.is_grasshopper", return_value=False) + mocker.patch("compas.scene.context.compas.is_rhino", return_value=False) - with pytest.raises(SceneObjectNotRegisteredError): - item = FakeSubItem() - _ = SceneObject(item) + with pytest.raises(SceneObjectNotRegisteredError): + item = FakeSubItem() + _ = SceneObject(item) diff --git a/tests/compas/scene/test_scene_serialisation.py b/tests/compas/scene/test_scene_serialisation.py index 2fd12c3bca1..26fb03f7e9f 100644 --- a/tests/compas/scene/test_scene_serialisation.py +++ b/tests/compas/scene/test_scene_serialisation.py @@ -1,119 +1,117 @@ -import pytest # noqa: F401 import compas -from compas.data import Data -from compas.scene import Scene -from compas.geometry import Box -from compas.geometry import Capsule -from compas.geometry import Circle -from compas.geometry import Cone -from compas.geometry import Cylinder -from compas.geometry import Ellipse -from compas.geometry import Frame -from compas.geometry import Line -from compas.geometry import Point -from compas.geometry import Polygon -from compas.geometry import Polyhedron -from compas.geometry import Polyline -from compas.geometry import Sphere -from compas.geometry import Torus -from compas.geometry import Vector -from compas.geometry import Plane -from compas.datastructures import Mesh -from compas.datastructures import Graph -from compas.datastructures import VolMesh - - -@pytest.fixture -def items(): - box = Box.from_width_height_depth(1, 1, 1) - capsule = Capsule(0.5, 1, Frame.worldXY()) - circle = Circle(1, Frame.worldXY()) - cone = Cone(1, 1, Frame.worldXY()) - cylinder = Cylinder(1, 1, Frame.worldXY()) - line = Line(Point(0, 0, 0), Point(1, 1, 1)) - point = Point(0, 0, 0) - polygon = Polygon.from_sides_and_radius_xy(5, 1) - polyhedron = Polyhedron.from_platonicsolid(4) - polyline = Polyline([[0, 0, 0], [1, 0, 0], [1, 0, 1]]) - sphere = Sphere(1) - torus = Torus(1, 0.3, Frame.worldXY()) - vector = Vector(0, 0, 1) - ellipse = Ellipse(1, 0.5, Frame.worldXY()) - frame = Frame.worldXY() - plane = Plane(Point(0, 0, 0), Vector(0, 0, 1)) - mesh = Mesh.from_polyhedron(8) - graph = Graph.from_nodes_and_edges([(0, 0, 0), (0, -1.5, 0), (-1, 1, 0), (1, 1, 0)], [(0, 1), (0, 2), (0, 3)]) - volmesh = VolMesh.from_meshgrid(1, 1, 1, 2, 2, 2) - - return [ - box, - capsule, - circle, - cone, - cylinder, - line, - point, - polygon, - polyhedron, - polyline, - sphere, - torus, - vector, - ellipse, - frame, - plane, - mesh, - graph, - volmesh, - ] - - -def assert_is_data_equal(obj1, obj2, path=""): - if type(obj1) is not type(obj2): - print("Type mismatch: {} != {} for {}:{} and {}:{}".format(type(obj1), type(obj2), path, obj1, path, obj2)) - return False - - if isinstance(obj1, (list, tuple)): - if len(obj1) != len(obj2): - print("Length mismatch: {} != {} for {} and {}".format(len(obj1), len(obj2), path, path)) +if not compas.IPY: + import pytest # noqa: F401 + from compas.data import Data + from compas.scene import Scene + from compas.geometry import Box + from compas.geometry import Capsule + from compas.geometry import Circle + from compas.geometry import Cone + from compas.geometry import Cylinder + from compas.geometry import Ellipse + from compas.geometry import Frame + from compas.geometry import Line + from compas.geometry import Point + from compas.geometry import Polygon + from compas.geometry import Polyhedron + from compas.geometry import Polyline + from compas.geometry import Sphere + from compas.geometry import Torus + from compas.geometry import Vector + from compas.geometry import Plane + from compas.datastructures import Mesh + from compas.datastructures import Graph + from compas.datastructures import VolMesh + + @pytest.fixture + def items(): + box = Box.from_width_height_depth(1, 1, 1) + capsule = Capsule(0.5, 1, Frame.worldXY()) + circle = Circle(1, Frame.worldXY()) + cone = Cone(1, 1, Frame.worldXY()) + cylinder = Cylinder(1, 1, Frame.worldXY()) + line = Line(Point(0, 0, 0), Point(1, 1, 1)) + point = Point(0, 0, 0) + polygon = Polygon.from_sides_and_radius_xy(5, 1) + polyhedron = Polyhedron.from_platonicsolid(4) + polyline = Polyline([[0, 0, 0], [1, 0, 0], [1, 0, 1]]) + sphere = Sphere(1) + torus = Torus(1, 0.3, Frame.worldXY()) + vector = Vector(0, 0, 1) + ellipse = Ellipse(1, 0.5, Frame.worldXY()) + frame = Frame.worldXY() + plane = Plane(Point(0, 0, 0), Vector(0, 0, 1)) + mesh = Mesh.from_polyhedron(8) + graph = Graph.from_nodes_and_edges([(0, 0, 0), (0, -1.5, 0), (-1, 1, 0), (1, 1, 0)], [(0, 1), (0, 2), (0, 3)]) + volmesh = VolMesh.from_meshgrid(1, 1, 1, 2, 2, 2) + + return [ + box, + capsule, + circle, + cone, + cylinder, + line, + point, + polygon, + polyhedron, + polyline, + sphere, + torus, + vector, + ellipse, + frame, + plane, + mesh, + graph, + volmesh, + ] + + def assert_is_data_equal(obj1, obj2, path=""): + if type(obj1) is not type(obj2): + print("Type mismatch: {} != {} for {}:{} and {}:{}".format(type(obj1), type(obj2), path, obj1, path, obj2)) return False - for i, (item1, item2) in enumerate(zip(obj1, obj2)): - if not assert_is_data_equal(item1, item2, path="{}[{}]".format(path, i)): + if isinstance(obj1, (list, tuple)): + if len(obj1) != len(obj2): + print("Length mismatch: {} != {} for {} and {}".format(len(obj1), len(obj2), path, path)) return False - return True + for i, (item1, item2) in enumerate(zip(obj1, obj2)): + if not assert_is_data_equal(item1, item2, path="{}[{}]".format(path, i)): + return False - elif isinstance(obj1, dict): - if set(obj1.keys()) != set(obj2.keys()): - print("Key mismatch: {} != {} for {} and {}".format(set(obj1.keys()), set(obj2.keys()), path, path)) - return False + return True - for key in obj1: - if not assert_is_data_equal(obj1[key], obj2[key], path='{}["{}"]'.format(path, key)): + elif isinstance(obj1, dict): + if set(obj1.keys()) != set(obj2.keys()): + print("Key mismatch: {} != {} for {} and {}".format(set(obj1.keys()), set(obj2.keys()), path, path)) return False - return True - - elif isinstance(obj1, Data): - return assert_is_data_equal(obj1.__data__, obj2.__data__, path="{}.__data__".format(path)) + for key in obj1: + if not assert_is_data_equal(obj1[key], obj2[key], path='{}["{}"]'.format(path, key)): + return False - else: - if obj1 != obj2: - print("Value mismatch: {} != {} for {}:{} and {}:{}".format(obj1, obj2, path, obj1, path, obj2)) - return False - else: return True + elif isinstance(obj1, Data): + return assert_is_data_equal(obj1.__data__, obj2.__data__, path="{}.__data__".format(path)) + + else: + if obj1 != obj2: + print("Value mismatch: {} != {} for {}:{} and {}:{}".format(obj1, obj2, path, obj1, path, obj2)) + return False + else: + return True -def test_scene_serialisation(items, mocker): - if compas.IPY: - mocker.patch("compas.is_rhino", return_value=False) + def test_scene_serialisation(items, mocker): + if compas.IPY: + mocker.patch("compas.is_rhino", return_value=False) - scene1 = Scene() - for item in items: - scene1.add(item) + scene1 = Scene() + for item in items: + scene1.add(item) - scene2 = Scene.from_jsonstring(scene1.to_jsonstring()) - assert assert_is_data_equal(scene1, scene2) + scene2 = Scene.from_jsonstring(scene1.to_jsonstring()) + assert assert_is_data_equal(scene1, scene2)