From 0ab775339996565f830fb45aad91a3e1bcea01ce Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 16 Jan 2024 22:50:32 +0100 Subject: [PATCH 01/21] add back faces_wo_cell and edges_wo_face --- src/compas/datastructures/cell_network/cell_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 9e04ce4a5d7..c1310714393 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -2325,7 +2325,7 @@ def edge_cells(self, edge): # u, v = edge # return None in self._plane[u][v].values() - def isolated_edges(self): + def edges_wo_face(self): """Find the edges that are not part of a face. Returns @@ -2974,7 +2974,7 @@ def face_cells(self, face): cells.append(cell) return cells - def isolated_faces(self): + def faces_wo_cell(self): """Find the faces that are not part of a cell. Returns From 05f73a56940709d13242e8c655411da71e35542f Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 16 Jan 2024 22:50:45 +0100 Subject: [PATCH 02/21] add tests back --- tests/compas/datastructures/test_cell_network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/compas/datastructures/test_cell_network.py b/tests/compas/datastructures/test_cell_network.py index c9476d591d9..4386ad50043 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_wo_cell()) == {11} + assert set(ds.edges_wo_face()) == {(13, 15), (12, 14)} + assert set(ds.nonmanifold_edges()) == {(6, 7), (4, 5), (5, 6), (4, 7)} From f0de8e731a6e4f7ae1fe3174688563b0e4923fac Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Wed, 17 Jan 2024 08:39:04 +0100 Subject: [PATCH 03/21] Update cell_network.py --- .../cell_network/cell_network.py | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 9e04ce4a5d7..edf353bc0c8 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -167,6 +167,7 @@ def __init__( 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} @@ -556,13 +557,15 @@ def add_edge(self, u, v, attr_dict=None, **kwattr): # @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 @@ -661,6 +664,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. @@ -679,35 +684,19 @@ def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): # 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 + 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: - # @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 + raise ValueError("Cannot add cell, faces {} can not be unified.".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: @@ -2054,9 +2043,11 @@ def edge_attribute(self, edge, name, value=None): raise KeyError(edge) u, v = edge - attr = self._edge[u][v] + attr = self._edge.get(u, {}).get(v, {}) + if value is not None: - attr[name] = value + attr.update({name : value}) + self._edge[u][v] = attr return if name in attr: return attr[name] @@ -2987,6 +2978,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. From 3efd4073201c3847a595e88e2235876da45adcc0 Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Wed, 17 Jan 2024 08:46:35 +0100 Subject: [PATCH 04/21] wo to without --- src/compas/datastructures/cell_network/cell_network.py | 4 ++-- tests/compas/datastructures/test_cell_network.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 8e5d802c994..6c456b3f799 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -2316,7 +2316,7 @@ def edge_cells(self, edge): # u, v = edge # return None in self._plane[u][v].values() - def edges_wo_face(self): + def edges_without_face(self): """Find the edges that are not part of a face. Returns @@ -2965,7 +2965,7 @@ def face_cells(self, face): cells.append(cell) return cells - def faces_wo_cell(self): + def faces_without_cell(self): """Find the faces that are not part of a cell. Returns diff --git a/tests/compas/datastructures/test_cell_network.py b/tests/compas/datastructures/test_cell_network.py index 4386ad50043..9963fad358c 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_wo_cell()) == {11} - assert set(ds.edges_wo_face()) == {(13, 15), (12, 14)} + assert set(ds.faces_without_cell()) == {11} + assert set(ds.edges_without_face()) == {(13, 15), (12, 14)} assert set(ds.nonmanifold_edges()) == {(6, 7), (4, 5), (5, 6), (4, 7)} From b707b0b9a2ab71ab6e3e36088a938641dea678bc Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Wed, 17 Jan 2024 08:48:48 +0100 Subject: [PATCH 05/21] lint --- src/compas/datastructures/cell_network/cell_network.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 6c456b3f799..580b0cb5709 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -561,11 +561,11 @@ def add_edge(self, u, v, attr_dict=None, **kwattr): # but I leave this to you as you have a better overview if v not in self._edge[u]: - self._edge[v][u] = {} + self._edge[v][u] = {} if v not in self._plane[u]: - self._plane[u][v] = {} + self._plane[u][v] = {} if u not in self._plane[v]: - self._plane[v][u] = {} + self._plane[v][u] = {} return u, v @@ -2046,7 +2046,7 @@ def edge_attribute(self, edge, name, value=None): attr = self._edge.get(u, {}).get(v, {}) if value is not None: - attr.update({name : value}) + attr.update({name: value}) self._edge[u][v] = attr return if name in attr: @@ -2978,7 +2978,7 @@ def faces_without_cell(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 + # # 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. From 3bb3c47f5e138194a3c466aa193674a1b4af2ba3 Mon Sep 17 00:00:00 2001 From: Romana Rust <117177043+romanavyzn@users.noreply.github.com> Date: Fri, 19 Jan 2024 08:03:52 +0100 Subject: [PATCH 06/21] edge to edge_data --- .../cell_network/cell_network.py | 28 ++++++++++--------- .../datastructures/test_cell_network.py | 4 +-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 580b0cb5709..42df3901396 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -230,6 +230,7 @@ def data(self): "edge": self._edge, "face": self._face, "cell": cell, + "edge_data": self._edge_data, "face_data": self._face_data, "cell_data": self._cell_data, "max_vertex": self._max_vertex, @@ -261,8 +262,10 @@ def from_data(cls, data): for key, attr in iter(vertex.items()): cell_network.add_vertex(key=key, attr_dict=attr) + edge_data = data.get("edge_data") or {} for u in edge: - for v, attr in edge[u].items(): + for v in edge[u]: + attr = edge_data.get(tuple(sorted((u, v))), {}) cell_network.add_edge(u, v, attr_dict=attr) face_data = data.get("face_data") or {} @@ -550,9 +553,11 @@ 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? @@ -1880,12 +1885,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 @@ -2042,12 +2048,11 @@ def edge_attribute(self, edge, name, value=None): if not self.has_edge(edge): raise KeyError(edge) - u, v = edge - attr = self._edge.get(u, {}).get(v, {}) + attr = self._edge_data.get(tuple(sorted(edge)), {}) if value is not None: attr.update({name: value}) - self._edge[u][v] = attr + self._edge_data[tuple(sorted(edge))] = attr return if name in attr: return attr[name] @@ -2086,8 +2091,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. @@ -2122,14 +2126,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) diff --git a/tests/compas/datastructures/test_cell_network.py b/tests/compas/datastructures/test_cell_network.py index 9963fad358c..8ea07c3dbab 100644 --- a/tests/compas/datastructures/test_cell_network.py +++ b/tests/compas/datastructures/test_cell_network.py @@ -81,5 +81,5 @@ def test_cell_network_boundary(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_without_cell()) == {11} - assert set(ds.edges_without_face()) == {(13, 15), (12, 14)} - assert set(ds.nonmanifold_edges()) == {(6, 7), (4, 5), (5, 6), (4, 7)} + assert set(ds.edges_without_face()) == {(15, 13), (14, 12)} + assert set(ds.nonmanifold_edges()) == {(6, 7), (4, 5), (5, 6), (7, 4)} From bcf8c2cc623cebccf836f3cf66e3f22f7e60a7b0 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 23 May 2024 11:11:58 +0200 Subject: [PATCH 07/21] Fix icp_numpy --- CHANGELOG.md | 4 ++++ src/compas/geometry/icp_numpy.py | 13 ++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d77a121de..e4467a3f3b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added `maxiter` parameter to `compas.geometry.icp_numpy`. + ### Changed +* Fixed bug in `compas.geometry.ic_numpy`, which was caused by returning only the last transformation of the iteration process. + ### Removed diff --git a/src/compas/geometry/icp_numpy.py b/src/compas/geometry/icp_numpy.py index 559b02f739a..ccd905ad351 100644 --- a/src/compas/geometry/icp_numpy.py +++ b/src/compas/geometry/icp_numpy.py @@ -2,6 +2,7 @@ from numpy import argmin from numpy import asarray from numpy.linalg import det +from numpy.linalg import multi_dot from scipy.linalg import norm from scipy.linalg import svd from scipy.spatial.distance import cdist @@ -36,7 +37,7 @@ def bestfit_transform(A, B): return X -def icp_numpy(source, target, tol=None): +def icp_numpy(source, target, tol=None, maxiter=100): """Align two point clouds using the Iterative Closest Point (ICP) method. Parameters @@ -48,6 +49,8 @@ def icp_numpy(source, target, tol=None): tol : float, optional Tolerance for finding matches. Default is :attr:`TOL.approximation`. + maxiter : int, optional + The maximum number of iterations. Returns ------- @@ -90,7 +93,9 @@ def icp_numpy(source, target, tol=None): X = Transformation.from_frame_to_frame(A_frame, B_frame) A = transform_points_numpy(A, X) - for i in range(20): + stack = [X] + + for i in range(maxiter): D = cdist(A, B, "euclidean") closest = argmin(D, axis=1) residual = norm(normrow(A - B[closest])) @@ -101,4 +106,6 @@ def icp_numpy(source, target, tol=None): X = bestfit_transform(A, B[closest]) A = transform_points_numpy(A, X) - return A, X + stack.append(X) + + return A, multi_dot(stack[::-1]) From 2516f86271125586d770038749cdff80ad5642c6 Mon Sep 17 00:00:00 2001 From: Romana Rust <117177043+romanavyzn@users.noreply.github.com> Date: Sun, 26 May 2024 08:38:48 +0200 Subject: [PATCH 08/21] updates --- .../basics.datastructures.cell_networks.py | 102 + .../basics.datastructures.cell_networks.rst | 30 + .../basics.datastructures.cellnetwork.rst | 74 + docs/userguide/network.json | 1 + docs/userguide/test.ipynb | 299 ++ docs/userguide/test.py | 62 + .../cell_network/cell_network copy.py | 4324 +++++++++++++++++ src/compas/scene/cellnetworkobject.py | 252 + 8 files changed, 5144 insertions(+) create mode 100644 docs/userguide/basics.datastructures.cell_networks.py create mode 100644 docs/userguide/basics.datastructures.cell_networks.rst create mode 100644 docs/userguide/basics.datastructures.cellnetwork.rst create mode 100644 docs/userguide/network.json create mode 100644 docs/userguide/test.ipynb create mode 100644 docs/userguide/test.py create mode 100644 src/compas/datastructures/cell_network/cell_network copy.py create mode 100644 src/compas/scene/cellnetworkobject.py diff --git a/docs/userguide/basics.datastructures.cell_networks.py b/docs/userguide/basics.datastructures.cell_networks.py new file mode 100644 index 00000000000..1d4db205e32 --- /dev/null +++ b/docs/userguide/basics.datastructures.cell_networks.py @@ -0,0 +1,102 @@ +from compas.geometry import Point, Vector, Frame, Box, close + +from scipy.spatial import cKDTree + +boxes = [Box(4.5, 9.5, 3.0, Frame(Point(2.250, 4.750, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), +Box(3.0, 5.0, 3.0, Frame(Point(1.500, 2.500, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), +Box(8.0, 4.5, 3.0, Frame(Point(4.000, 2.250, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), +Box(5.0, 5.0, 3.0, Frame(Point(2.500, 2.500, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), +Box(7.5, 6.5, 3.0, Frame(Point(3.750, 3.250, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), +Box(5.0, 6.5, 3.0, Frame(Point(2.500, 3.250, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000)))] + +tt_faces = [[Point(5.000, 8.000, 6.000), Point(0.000, 8.000, 6.000), Point(5.000, 3.000, 6.000), Point(0.000, 3.000, 6.000)], +[Point(14.500, -1.500, 6.000), Point(14.500, -1.500, 3.000), Point(12.500, -1.500, 6.000), Point(12.500, -1.500, 3.000)], +[Point(12.500, -1.500, 6.000), Point(12.500, 8.000, 6.000), Point(14.500, 8.000, 6.000), Point(14.500, -1.500, 6.000), Point(12.500, -1.500, 6.000)], +[Point(12.500, -1.500, 3.000), Point(12.500, 8.000, 3.000), Point(14.500, 8.000, 3.000), Point(14.500, -1.500, 3.000), Point(12.500, -1.500, 3.000)], +[Point(14.500, 8.000, 4.200), Point(14.500, 8.000, 3.000), Point(14.500, -1.500, 4.200), Point(14.500, -1.500, 3.000)], +[Point(12.500, 8.000, 6.000), Point(12.500, 8.000, 3.000), Point(14.500, 8.000, 6.000), Point(14.500, 8.000, 3.000)], +] + +points = [] +for box in boxes: + for pt in box.to_vertices_and_faces()[0]: + if not len(points): + points.append(pt) + continue + + tree = cKDTree(points) + d, idx = tree.query(pt, k=1) + if close(d, 0, 1e-3): + pass + else: + points.append(pt) + +for face in tt_faces: + for pt in face: + tree = cKDTree(points) + d, idx = tree.query(pt, k=1) + if close(d, 0, 1e-3): + pass + else: + points.append(pt) + +print(len(points)) +tree = cKDTree(points) + +from compas.datastructures import CellNetwork + +network = CellNetwork() + +for i, (x, y, z) in enumerate(points): + network.add_vertex(key=i, x=x, y=y, z=z) + +for box in boxes: + verts, faces = box.to_vertices_and_faces() + fidxs = [] + for face in faces: + nface = [] + for idx in face: + pt = verts[idx] + d, ni = tree.query(pt, k=1) + nface.append(ni) + fidx = None + for face in network.faces(): + if set( network.face_vertices(face)) == set(nface): + fidx = face + break + else: + fidx = network.add_face(nface) + fidxs.append(fidx) + network.add_cell(fidxs) + print(fidxs) + + +for face in tt_faces: + + nface = [] + for pt in face: + d, ni = tree.query(pt, k=1) + nface.append(ni) + + fidx = None + for face in network.faces(): + if set( network.face_vertices(face)) == set(nface): + fidx = face + break + else: + fidx = network.add_face(nface) + + +from compas_viewer import Viewer + +viewer = Viewer() + +viewer.add(network, show_faces=True, show_edges=True, show_vertices=True) +viewer.show() + +""" + [network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] + [network.add_edge(u, v) for u, v in edges] + [network.add_face(fverts) for fverts in faces] + [network.add_cell(fkeys) for fkeys in cells] +""" diff --git a/docs/userguide/basics.datastructures.cell_networks.rst b/docs/userguide/basics.datastructures.cell_networks.rst new file mode 100644 index 00000000000..4bcb5a62391 --- /dev/null +++ b/docs/userguide/basics.datastructures.cell_networks.rst @@ -0,0 +1,30 @@ +******************************************************************************** +Cell Networks +******************************************************************************** + +.. rst-class:: lead + +A `compas.datastructures.CellNetwork` is a geometric implementation of a data structure for storing a +collection of mixed topologic entities such as cells, faces, edges and vertices. +It can be used to describe buildings; walls and floors can be represented as faces, columns, beams as edges, and rooms as cells. +Aperatures such as windows or doors could be stored as face attributes. +Topological queries such as "what is the building envelope" can be easily be derived. + +.. note:: + + Please refer to the API for a complete overview of all functionality: + + * :class:`compas.datastructures.HalfFace` + * :class:`compas.datastructures.CellNetwork` + + +CellNetwork Construction +======================== + +Cell networks 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. + + diff --git a/docs/userguide/basics.datastructures.cellnetwork.rst b/docs/userguide/basics.datastructures.cellnetwork.rst new file mode 100644 index 00000000000..a8e7358e1c1 --- /dev/null +++ b/docs/userguide/basics.datastructures.cellnetwork.rst @@ -0,0 +1,74 @@ +******************************************************************************** +Cell Networks +******************************************************************************** + +.. rst-class:: lead + +A :class:`compas.datastructures.CellNetwork` uses a halfface data structure to represent a collection of mixed topologic entities such as cells, faces, edges and nodes, +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` + + +CellNetwork Construction +================= + +Meshes 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]] +>>> [cell_network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] +>>> [cell_network.add_face(fverts) for fverts in faces] +>>> [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`. + +>>> from compas.datastructures import CellNetwork +>>> from compas.scene import Scene +>>> cell_network = CellNetwork.from_json(compas.get('tubemesh.obj')) +>>> scene = Scene() +>>> scene.add(mesh) +>>> scene.show() + + + + + + + diff --git a/docs/userguide/network.json b/docs/userguide/network.json new file mode 100644 index 00000000000..4b2807af9b7 --- /dev/null +++ b/docs/userguide/network.json @@ -0,0 +1 @@ +{"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": 0.0, "y": 0.0, "z": 0.0}, "1": {"x": 0.0, "y": 9.5, "z": 0.0}, "2": {"x": 4.5, "y": 9.5, "z": 0.0}, "3": {"x": 4.5, "y": 0.0, "z": 0.0}, "4": {"x": 0.0, "y": 0.0, "z": 3.0}, "5": {"x": 4.5, "y": 0.0, "z": 3.0}, "6": {"x": 4.5, "y": 9.5, "z": 3.0}, "7": {"x": 0.0, "y": 9.5, "z": 3.0}, "8": {"x": 5.0, "y": 8.0, "z": 6.0}, "9": {"x": 0.0, "y": 8.0, "z": 6.0}, "10": {"x": 5.0, "y": 3.0, "z": 6.0}, "11": {"x": 0.0, "y": 3.0, "z": 6.0}, "12": {"x": 14.5, "y": -1.5, "z": 6.0}, "13": {"x": 14.5, "y": -1.5, "z": 3.0}, "14": {"x": 12.5, "y": -1.5, "z": 6.0}, "15": {"x": 12.5, "y": -1.5, "z": 3.0}, "16": {"x": 12.5, "y": 8.0, "z": 6.0}, "17": {"x": 14.5, "y": 8.0, "z": 6.0}, "18": {"x": 12.5, "y": 8.0, "z": 3.0}, "19": {"x": 14.5, "y": 8.0, "z": 3.0}, "20": {"x": 14.5, "y": 8.0, "z": 4.2}, "21": {"x": 14.5, "y": -1.5, "z": 4.2}}, "edge": {"0": {"3": {}, "4": {}}, "1": {"0": {}}, "2": {"1": {}}, "3": {"2": {}}, "4": {"5": {}}, "5": {"3": {}, "6": {}}, "6": {"2": {}, "7": {}}, "7": {"1": {}, "4": {}}, "8": {"11": {}}, "9": {"8": {}}, "10": {"9": {}}, "11": {"10": {}}, "12": {"15": {}, "17": {}}, "13": {"12": {}, "19": {}, "21": {}}, "14": {"13": {}, "12": {}}, "15": {"14": {}, "13": {}}, "16": {"14": {}, "19": {}}, "17": {"16": {}, "18": {}}, "18": {"15": {}, "16": {}}, "19": {"18": {}, "20": {}, "17": {}}, "20": {"13": {}}, "21": {"19": {}}}, "face": {"0": [0, 1, 2, 3], "1": [0, 3, 5, 4], "2": [3, 2, 6, 5], "3": [2, 1, 7, 6], "4": [1, 0, 4, 7], "5": [4, 5, 6, 7], "6": [8, 9, 10, 11], "7": [12, 13, 14, 15], "8": [14, 16, 17, 12], "9": [15, 18, 19, 13], "10": [20, 19, 21, 13], "11": [16, 18, 17, 19]}, "cell": {"0": [0, 1, 2, 3, 4, 5]}, "face_data": {}, "cell_data": {}, "max_vertex": 21, "max_face": 11, "max_cell": 0}, "guid": "dc7bbd81-65a7-438c-a653-66ee653cf1d1"} \ No newline at end of file diff --git a/docs/userguide/test.ipynb b/docs/userguide/test.ipynb new file mode 100644 index 00000000000..ec6cfdf3fc1 --- /dev/null +++ b/docs/userguide/test.ipynb @@ -0,0 +1,299 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22\n", + "[0, 1, 2, 3, 4, 5]\n" + ] + } + ], + "source": [ + "from compas.geometry import Point, Vector, Frame, Box, close\n", + "\n", + "from scipy.spatial import cKDTree\n", + "\n", + "boxes = [Box(4.5, 9.5, 3.0, Frame(Point(2.250, 4.750, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", + "#Box(3.0, 5.0, 3.0, Frame(Point(1.500, 2.500, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", + "]\n", + "#Box(8.0, 4.5, 3.0, Frame(Point(4.000, 2.250, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", + "#Box(5.0, 5.0, 3.0, Frame(Point(2.500, 2.500, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", + "#Box(7.5, 6.5, 3.0, Frame(Point(3.750, 3.250, 4.50), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", + "#Box(5.0, 6.5, 3.0, Frame(Point(2.500, 3.250, 4.50), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000)))]\n", + "\n", + "tt_faces = [[Point(5.000, 8.000, 6.000), Point(0.000, 8.000, 6.000), Point(5.000, 3.000, 6.000), Point(0.000, 3.000, 6.000)],\n", + "[Point(14.500, -1.500, 6.000), Point(14.500, -1.500, 3.000), Point(12.500, -1.500, 6.000), Point(12.500, -1.500, 3.000)],\n", + "[Point(12.500, -1.500, 6.000), Point(12.500, 8.000, 6.000), Point(14.500, 8.000, 6.000), Point(14.500, -1.500, 6.000), Point(12.500, -1.500, 6.000)],\n", + "[Point(12.500, -1.500, 3.000), Point(12.500, 8.000, 3.000), Point(14.500, 8.000, 3.000), Point(14.500, -1.500, 3.000), Point(12.500, -1.500, 3.000)],\n", + "[Point(14.500, 8.000, 4.200), Point(14.500, 8.000, 3.000), Point(14.500, -1.500, 4.200), Point(14.500, -1.500, 3.000)],\n", + "[Point(12.500, 8.000, 6.000), Point(12.500, 8.000, 3.000), Point(14.500, 8.000, 6.000), Point(14.500, 8.000, 3.000)],\n", + "]\n", + "\n", + "points = []\n", + "for box in boxes:\n", + " for pt in box.to_vertices_and_faces()[0]:\n", + " if not len(points):\n", + " points.append(pt)\n", + " continue\n", + "\n", + " tree = cKDTree(points)\n", + " d, idx = tree.query(pt, k=1)\n", + " if close(d, 0, 1e-3):\n", + " pass\n", + " else:\n", + " points.append(pt)\n", + "\n", + "for face in tt_faces:\n", + " for pt in face:\n", + " tree = cKDTree(points)\n", + " d, idx = tree.query(pt, k=1)\n", + " if close(d, 0, 1e-3):\n", + " pass\n", + " else:\n", + " points.append(pt)\n", + "\n", + "print(len(points))\n", + "tree = cKDTree(points)\n", + "\n", + "from compas.datastructures import CellNetwork\n", + "\n", + "network = CellNetwork()\n", + "\n", + "for i, (x, y, z) in enumerate(points):\n", + " network.add_vertex(key=i, x=x, y=y, z=z)\n", + "\n", + "for box in boxes:\n", + " verts, faces = box.to_vertices_and_faces()\n", + " fidxs = []\n", + " for face in faces:\n", + " nface = []\n", + " for idx in face:\n", + " pt = verts[idx]\n", + " d, ni = tree.query(pt, k=1)\n", + " nface.append(ni)\n", + " fidx = None\n", + " for face in network.faces():\n", + " if set( network.face_vertices(face)) == set(nface):\n", + " fidx = face\n", + " break\n", + " else:\n", + " fidx = network.add_face(nface)\n", + " fidxs.append(fidx)\n", + " network.add_cell(fidxs)\n", + " print(fidxs)\n", + "\n", + "\n", + "for face in tt_faces:\n", + "\n", + " nface = []\n", + " for pt in face:\n", + " d, ni = tree.query(pt, k=1)\n", + " nface.append(ni)\n", + "\n", + " fidx = None\n", + " for face in network.faces():\n", + " if set( network.face_vertices(face)) == set(nface):\n", + " fidx = face\n", + " break\n", + " else:\n", + " fidx = network.add_face(nface)\n", + "\n", + "\n", + "\n", + "\n", + "\"\"\"\n", + " [network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices]\n", + " [network.add_edge(u, v) for u, v in edges]\n", + " [network.add_face(fverts) for fverts in faces]\n", + " [network.add_cell(fkeys) for fkeys in cells]\n", + "\"\"\"\n", + "network.to_json('/Users/romanarust/workspace/compas_dev/compas/docs/userguide/network.json')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[0, 1, 2, 3], [0, 3, 5, 4], [3, 2, 6, 5], [2, 1, 7, 6], [1, 0, 4, 7], [4, 5, 6, 7]]\n", + "\n" + ] + } + ], + "source": [ + "from compas.geometry import Box\n", + "from compas.datastructures import CellNetwork\n", + "\n", + "network = CellNetwork()\n", + "\n", + "vertices, faces = Box(1).to_vertices_and_faces()\n", + "for x, y, z in vertices:\n", + " network.add_vertex(x=x, y=y, z=z)\n", + "print(faces)\n", + "fkeys = []\n", + "for face in faces:\n", + " fkeys.append(network.add_face(face))\n", + "network.add_cell(fkeys)\n", + "\n", + "\n", + "print(network)\n", + "\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8\n" + ] + }, + { + "ename": "KeyError", + "evalue": "0", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[7], line 9\u001b[0m\n\u001b[1;32m 7\u001b[0m cells \u001b[38;5;241m=\u001b[39m [[\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m3\u001b[39m, \u001b[38;5;241m4\u001b[39m, \u001b[38;5;241m5\u001b[39m]]\n\u001b[1;32m 8\u001b[0m [network\u001b[38;5;241m.\u001b[39madd_vertex(x\u001b[38;5;241m=\u001b[39mx, y\u001b[38;5;241m=\u001b[39my, z\u001b[38;5;241m=\u001b[39mz) \u001b[38;5;28;01mfor\u001b[39;00m x, y, z \u001b[38;5;129;01min\u001b[39;00m vertices]\n\u001b[0;32m----> 9\u001b[0m [cell_network\u001b[38;5;241m.\u001b[39madd_face(fverts) \u001b[38;5;28;01mfor\u001b[39;00m fverts \u001b[38;5;129;01min\u001b[39;00m faces]\n\u001b[1;32m 10\u001b[0m [cell_network\u001b[38;5;241m.\u001b[39madd_cell(fkeys) \u001b[38;5;28;01mfor\u001b[39;00m fkeys \u001b[38;5;129;01min\u001b[39;00m cells]\n\u001b[1;32m 11\u001b[0m cell_network\n", + "Cell \u001b[0;32mIn[7], line 9\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 7\u001b[0m cells \u001b[38;5;241m=\u001b[39m [[\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m3\u001b[39m, \u001b[38;5;241m4\u001b[39m, \u001b[38;5;241m5\u001b[39m]]\n\u001b[1;32m 8\u001b[0m [network\u001b[38;5;241m.\u001b[39madd_vertex(x\u001b[38;5;241m=\u001b[39mx, y\u001b[38;5;241m=\u001b[39my, z\u001b[38;5;241m=\u001b[39mz) \u001b[38;5;28;01mfor\u001b[39;00m x, y, z \u001b[38;5;129;01min\u001b[39;00m vertices]\n\u001b[0;32m----> 9\u001b[0m [\u001b[43mcell_network\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd_face\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfverts\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m fverts \u001b[38;5;129;01min\u001b[39;00m faces]\n\u001b[1;32m 10\u001b[0m [cell_network\u001b[38;5;241m.\u001b[39madd_cell(fkeys) \u001b[38;5;28;01mfor\u001b[39;00m fkeys \u001b[38;5;129;01min\u001b[39;00m cells]\n\u001b[1;32m 11\u001b[0m cell_network\n", + "File \u001b[0;32m~/workspace/compas_dev/compas/src/compas/datastructures/cell_network/cell_network.py:714\u001b[0m, in \u001b[0;36mCellNetwork.add_face\u001b[0;34m(self, vertices, fkey, attr_dict, **kwattr)\u001b[0m\n\u001b[1;32m 711\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mface_attribute(fkey, name, value)\n\u001b[1;32m 713\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m u, v \u001b[38;5;129;01min\u001b[39;00m pairwise(vertices \u001b[38;5;241m+\u001b[39m vertices[:\u001b[38;5;241m1\u001b[39m]):\n\u001b[0;32m--> 714\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m v \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_plane\u001b[49m\u001b[43m[\u001b[49m\u001b[43mu\u001b[49m\u001b[43m]\u001b[49m:\n\u001b[1;32m 715\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_plane[u][v] \u001b[38;5;241m=\u001b[39m {}\n\u001b[1;32m 716\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_plane[u][v][fkey] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", + "\u001b[0;31mKeyError\u001b[0m: 0" + ] + } + ], + "source": [ + "from compas.datastructures import CellNetwork\n", + "cell_network = CellNetwork()\n", + "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)]\n", + "\n", + "print(len(vertices))\n", + "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]]\n", + "cells = [[0, 1, 2, 3, 4, 5]]\n", + "[network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices]\n", + "[cell_network.add_face(fverts) for fverts in faces]\n", + "[cell_network.add_cell(fkeys) for fkeys in cells]\n", + "cell_network" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PyThreeJS SceneObjects registered.\n", + "No triangles found\n", + "No triangles found\n", + "No triangles found\n", + "No triangles found\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4d588dd468c842ebb17463175eb8cfcb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(Button(icon='folder-open', layout=Layout(height='32px', width='48px'), style=But…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from compas_notebook.viewer import Viewer\n", + "viewer = Viewer()\n", + "#viewer.scene.add(mesh, color='#cccccc')\n", + "\n", + "cell_network = network\n", + "from compas.geometry import Polygon, Polyhedron\n", + "\n", + "opacity = 0.5\n", + "\n", + "\n", + "\"\"\" \n", + "for face in cell_network.faces_on_boundaries():\n", + " vertices = cell_network.face_coordinates(face)\n", + " viewer.scene.add(Polygon(vertices), color='#cccccc')\n", + "\n", + "\n", + "for face in cell_network.faces_without_cell():\n", + " vertices = cell_network.face_coordinates(face)\n", + " viewer.scene.add(Polygon(vertices), color=\"#7d7eff\")\n", + "\"\"\"\n", + "\n", + "for face in cell_network.faces():\n", + "\n", + " vertices = cell_network.face_coordinates(face)\n", + " try:\n", + " viewer.scene.add(Polygon(vertices), color=\"#7d7eff\")\n", + " except:\n", + " print(face, vertices)\n", + "\n", + "\n", + "\"\"\"\n", + "for face in cell_network.faces():\n", + " cells = cell_network.face_cells(face)\n", + " vertices = cell_network.face_coordinates(face)\n", + "\n", + " if not len(cells):\n", + " pass\n", + " # viewer.add(Polygon(vertices), facecolor=[0.5, 0.55, 1.0], opacity=opacity)\n", + "\n", + " elif len(cells) == 2:\n", + " pass\n", + " # viewer.add(Polygon(vertices), facecolor=[1, 0.8, 0.05], opacity=opacity)\n", + " # break\n", + "\"\"\"\n", + "\n", + "\n", + "\n", + "\n", + "viewer.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "compas-dev", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/userguide/test.py b/docs/userguide/test.py new file mode 100644 index 00000000000..aaf0ab2528e --- /dev/null +++ b/docs/userguide/test.py @@ -0,0 +1,62 @@ +from compas.datastructures import CellNetwork +file = "/Users/romanarust/workspace/compas_dev/compas/docs/userguide/network.json" + +network = CellNetwork.from_json(file) + +from compas.geometry import Polygon + +#import compas_view2 + +#print(compas_view2.__file__) +#from compas_view2.app import App + +def show(cell_network): + + + viewer = App() + + opacity = 0.5 + + for face in cell_network.faces_on_boundaries(): + vertices = cell_network.face_coordinates(face) + viewer.add(Polygon(vertices), facecolor=[0.75, 0.75, 0.75], opacity=opacity) + + for face in cell_network.faces_no_cell(): + vertices = cell_network.face_coordinates(face) + viewer.add(Polygon(vertices), facecolor=[0.5, 0.55, 1.0], opacity=opacity) + + for face in cell_network.faces(): + cells = cell_network.face_cells(face) + vertices = cell_network.face_coordinates(face) + + if not len(cells): + pass + # viewer.add(Polygon(vertices), facecolor=[0.5, 0.55, 1.0], opacity=opacity) + + elif len(cells) == 2: + pass + # viewer.add(Polygon(vertices), facecolor=[1, 0.8, 0.05], opacity=opacity) + # break + + viewer.add(cell_network.to_network(), show_lines=True) + viewer.view.camera.zoom_extents() + viewer.show() + +#show(network) + + +from compas.scene import Scene + + + +scene = Scene() +scene.clear() +#scene.add(network) +opacity = 0.5 +cell_network = network + +for face in cell_network.faces_on_boundaries(): + vertices = cell_network.face_coordinates(face) + scene.add(Polygon(vertices), facecolor=[0.75, 0.75, 0.75], opacity=opacity) + +scene.draw() diff --git a/src/compas/datastructures/cell_network/cell_network copy.py b/src/compas/datastructures/cell_network/cell_network copy.py new file mode 100644 index 00000000000..f71bf6174e1 --- /dev/null +++ b/src/compas/datastructures/cell_network/cell_network copy.py @@ -0,0 +1,4324 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from random import sample + +from compas.datastructures import Mesh +from compas.datastructures import Graph +from compas.datastructures.datastructure import Datastructure +from compas.datastructures.attributes import VertexAttributeView +from compas.datastructures.attributes import EdgeAttributeView +from compas.datastructures.attributes import FaceAttributeView +from compas.datastructures.attributes import CellAttributeView + +from compas.files import OBJ + +from compas.geometry import Line +from compas.geometry import Point +from compas.geometry import Polygon +from compas.geometry import Polyhedron +from compas.geometry import Vector +from compas.geometry import centroid_points +from compas.geometry import centroid_polygon +from compas.geometry import centroid_polyhedron +from compas.geometry import distance_point_point +from compas.geometry import length_vector +from compas.geometry import normal_polygon +from compas.geometry import normalize_vector +from compas.geometry import volume_polyhedron +from compas.geometry import add_vectors +from compas.geometry import bestfit_plane +from compas.geometry import project_point_plane +from compas.geometry import scale_vector +from compas.geometry import subtract_vectors +from compas.geometry import bounding_box + +from compas.utilities import pairwise + +from compas.tolerance import TOL + + +class CellNetwork(Datastructure): + """Geometric implementation of a data structure for a collection of mixed topologic entities such as cells, faces, edges and nodes. + + Parameters + ---------- + default_vertex_attributes: dict, optional + Default values for vertex attributes. + default_edge_attributes: dict, optional + Default values for edge attributes. + default_face_attributes: dict, optional + Default values for face attributes. + default_cell_attributes: dict, optional + Default values for cell attributes. + name : str, optional + The name of the cell network. + **kwargs : dict, optional + Additional keyword arguments, which are stored in the attributes dict. + + Attributes + ---------- + default_vertex_attributes : dict[str, Any] + Default attributes of the vertices. + default_edge_attributes: dict[str, Any] + Default values for edge attributes. + default_face_attributes: dict[str, Any] + Default values for face attributes. + default_cell_attributes: dict[str, Any] + Default values for cell attributes. + + Examples + -------- + >>> 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]] + >>> [network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] + >>> [cell_network.add_face(fverts) for fverts in faces] + >>> [cell_network.add_cell(fkeys) for fkeys in cells] + >>> cell_network + + """ + + DATASCHEMA = { + "type": "object", + "properties": { + "attributes": {"type": "object"}, + "default_vertex_attributes": {"type": "object"}, + "default_edge_attributes": {"type": "object"}, + "default_face_attributes": {"type": "object"}, + "default_cell_attributes": {"type": "object"}, + "vertex": { + "type": "object", + "patternProperties": {"^[0-9]+$": {"type": "object"}}, + "additionalProperties": False, + }, + "edge": { + "type": "object", + "patternProperties": { + "^[0-9]+$": { + "type": "object", + "patternProperties": {"^[0-9]+$": {"type": "object"}}, + "additionalProperties": False, + } + }, + "additionalProperties": False, + }, + "face": { + "type": "object", + "patternProperties": { + "^[0-9]+$": { + "type": "array", + "items": {"type": "integer", "minimum": 0}, + "minItems": 3, + } + }, + "additionalProperties": False, + }, + "cell": { + "type": "object", + "patternProperties": { + "^[0-9]+$": { + "type": "array", + "minItems": 4, + "items": { + "type": "array", + "minItems": 3, + "items": {"type": "integer", "minimum": 0}, + }, + } + }, + "additionalProperties": False, + }, + "face_data": { + "type": "object", + "patternProperties": {"^\\([0-9]+(, [0-9]+){3, }\\)$": {"type": "object"}}, + "additionalProperties": False, + }, + "cell_data": { + "type": "object", + "patternProperties": {"^[0-9]+$": {"type": "object"}}, + "additionalProperties": False, + }, + "max_vertex": {"type": "number", "minimum": -1}, + "max_face": {"type": "number", "minimum": -1}, + "max_cell": {"type": "number", "minimum": -1}, + }, + "required": [ + "attributes", + "default_vertex_attributes", + "default_edge_attributes", + "default_face_attributes", + "default_cell_attributes", + "vertex", + "edge", + "face", + "cell", + "face_data", + "cell_data", + "max_vertex", + "max_face", + "max_cell", + ], + } + + @property + def __data__(self): + cell = {} + for c in self._cell: + faces = set() + for u in self._cell[c]: + for v in self._cell[c][u]: + faces.add(self._cell[c][u][v]) + cell[c] = sorted(list(faces)) + + return { + "attributes": self.attributes, + "default_vertex_attributes": self.default_vertex_attributes, + "default_edge_attributes": self.default_edge_attributes, + "default_face_attributes": self.default_face_attributes, + "default_cell_attributes": self.default_cell_attributes, + "vertex": self._vertex, + "edge": self._edge, + "face": self._face, + "cell": cell, + "face_data": self._face_data, + "cell_data": self._cell_data, + "max_vertex": self._max_vertex, + "max_face": self._max_face, + "max_cell": self._max_cell, + } + + @classmethod + def __from_data__(cls, data): + cell_network = cls( + default_vertex_attributes=data.get("default_vertex_attributes"), + default_edge_attributes=data.get("default_edge_attributes"), + default_face_attributes=data.get("default_face_attributes"), + default_cell_attributes=data.get("default_cell_attributes"), + ) + cell_network.attributes.update(data.get("attributes") or {}) + + vertex = data["vertex"] or {} + edge = data["edge"] or {} + face = data["face"] or {} + cell = data["cell"] or {} + + for key, attr in iter(vertex.items()): + cell_network.add_vertex(key=key, attr_dict=attr) + + for u in edge: + for v, attr in edge[u].items(): + cell_network.add_edge(u, 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_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._max_vertex = data.get("max_vertex", cell_network._max_vertex) + cell_network._max_face = data.get("max_face", cell_network._max_face) + cell_network._max_cell = data.get("max_cell", cell_network._max_cell) + + return cell_network + + def __init__( + self, + default_vertex_attributes=None, + default_edge_attributes=None, + default_face_attributes=None, + default_cell_attributes=None, + name=None, + **kwargs + ): + super(CellNetwork, self).__init__(kwargs, name=name) + self._max_vertex = -1 + self._max_face = -1 + self._max_cell = -1 + self._vertex = {} + self._edge = {} + 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} + self.default_edge_attributes = {} + self.default_face_attributes = {} + self.default_cell_attributes = {} + if default_vertex_attributes: + self.default_vertex_attributes.update(default_vertex_attributes) + if default_edge_attributes: + self.default_edge_attributes.update(default_edge_attributes) + if default_face_attributes: + self.default_face_attributes.update(default_face_attributes) + if default_cell_attributes: + self.default_cell_attributes.update(default_cell_attributes) + + def __str__(self): + tpl = "" + return tpl.format( + self.number_of_vertices(), + self.number_of_faces(), + self.number_of_cells(), + self.number_of_edges(), + ) + + # -------------------------------------------------------------------------- + # Data + # -------------------------------------------------------------------------- + + @property + def data(self): + """Returns a dictionary of structured data representing the cell network data object. + + Note that some of the data stored internally in the data structure object is not included in the dictionary representation of the object. + This is the case for data that is considered private and/or redundant. + Specifically, the plane dictionary are not included. + This is because the information in these dictionaries can be reconstructed from the other data. + Therefore, to keep the dictionary representation as compact as possible, these dictionaries are not included. + + Returns + ------- + dict + The structured data representing the cell network. + + """ + cell = {} + for c in self._cell: + faces = set() + for u in self._cell[c]: + for v in self._cell[c][u]: + faces.add(self._cell[c][u][v]) + cell[c] = sorted(list(faces)) + + return { + "attributes": self.attributes, + "dva": self.default_vertex_attributes, + "dea": self.default_edge_attributes, + "dfa": self.default_face_attributes, + "dca": self.default_cell_attributes, + "vertex": self._vertex, + "edge": self._edge, + "face": self._face, + "cell": cell, + "edge_data": self._edge_data, + "face_data": self._face_data, + "cell_data": self._cell_data, + "max_vertex": self._max_vertex, + "max_face": self._max_face, + "max_cell": self._max_cell, + } + + @classmethod + def from_data(cls, data): + dva = data.get("dva") or {} + dea = data.get("dea") or {} + dfa = data.get("dfa") or {} + dca = data.get("dca") or {} + + cell_network = cls( + default_vertex_attributes=dva, + default_edge_attributes=dea, + default_face_attributes=dfa, + default_cell_attributes=dca, + ) + + cell_network.attributes.update(data.get("attributes") or {}) + + vertex = data.get("vertex") or {} + edge = data.get("edge") or {} + face = data.get("face") or {} + cell = data.get("cell") or {} + + for key, attr in iter(vertex.items()): + cell_network.add_vertex(key=key, attr_dict=attr) + + edge_data = data.get("edge_data") or {} + for u in edge: + for v in edge[u]: + attr = edge_data.get(tuple(sorted((u, v))), {}) + cell_network.add_edge(u, v, attr_dict=attr) + + face_data = data.get("face_data") or {} + for key, vertices in iter(face.items()): + attr = face_data.get(key) or {} + cell_network.add_face(vertices, fkey=key, attr_dict=attr) + + cell_data = data.get("cell_data") or {} + for ckey, faces in iter(cell.items()): + attr = cell_data.get(ckey) or {} + cell_network.add_cell(faces, ckey=ckey, attr_dict=attr) + + cell_network._max_vertex = data.get("max_vertex", cell_network._max_vertex) + cell_network._max_face = data.get("max_face", cell_network._max_face) + cell_network._max_cell = data.get("max_cell", cell_network._max_cell) + + return cell_network + + # -------------------------------------------------------------------------- + # Helpers + # -------------------------------------------------------------------------- + + def clear(self): + """Clear all the volmesh data. + + Returns + ------- + None + + """ + del self._vertex + del self._edge + del self._face + del self._cell + del self._plane + del self._face_data + del self._cell_data + self._vertex = {} + self._edge = {} + self._face = {} + self._cell = {} + self._plane = {} + self._face_data = {} + self._cell_data = {} + self._max_vertex = -1 + self._max_face = -1 + self._max_cell = -1 + + def vertex_sample(self, size=1): + """Get the identifiers of a set of random vertices. + + Parameters + ---------- + size : int, optional + The size of the sample. + + Returns + ------- + list[int] + The identifiers of the vertices. + + See Also + -------- + :meth:`edge_sample`, :meth:`face_sample`, :meth:`cell_sample` + + """ + return sample(list(self.vertices()), size) + + def edge_sample(self, size=1): + """Get the identifiers of a set of random edges. + + Parameters + ---------- + size : int, optional + The size of the sample. + + Returns + ------- + list[tuple[int, int]] + The identifiers of the edges. + + See Also + -------- + :meth:`vertex_sample`, :meth:`face_sample`, :meth:`cell_sample` + + """ + return sample(list(self.edges()), size) + + def face_sample(self, size=1): + """Get the identifiers of a set of random faces. + + Parameters + ---------- + size : int, optional + The size of the sample. + + Returns + ------- + list[int] + The identifiers of the faces. + + See Also + -------- + :meth:`vertex_sample`, :meth:`edge_sample`, :meth:`cell_sample` + + """ + return sample(list(self.faces()), size) + + def cell_sample(self, size=1): + """Get the identifiers of a set of random cells. + + Parameters + ---------- + size : int, optional + The size of the sample. + + Returns + ------- + list[int] + The identifiers of the cells. + + See Also + -------- + :meth:`vertex_sample`, :meth:`edge_sample`, :meth:`face_sample` + + """ + return sample(list(self.cells()), size) + + def vertex_index(self): + """Returns a dictionary that maps vertex identifiers to the corresponding index in a vertex list or array. + + Returns + ------- + dict[int, int] + A dictionary of vertex-index pairs. + + See Also + -------- + :meth:`index_vertex` + + """ + return {key: index for index, key in enumerate(self.vertices())} + + def index_vertex(self): + """Returns a dictionary that maps the indices of a vertex list to vertex identifiers. + + Returns + ------- + dict[int, int] + A dictionary of index-vertex pairs. + + See Also + -------- + :meth:`vertex_index` + + """ + return dict(enumerate(self.vertices())) + + def vertex_gkey(self, precision=None): + """Returns a dictionary that maps vertex identifiers to the corresponding *geometric key* up to a certain precision. + + Parameters + ---------- + precision : int, optional + Precision for converting numbers to strings. + Default is :attr:`TOL.precision`. + + Returns + ------- + dict[int, str] + A dictionary of vertex-geometric key pairs. + + See Also + -------- + :meth:`gkey_vertex` + + """ + gkey = TOL.geometric_key + xyz = self.vertex_coordinates + return {vertex: gkey(xyz(vertex), precision) for vertex in self.vertices()} + + def gkey_vertex(self, precision=None): + """Returns a dictionary that maps *geometric keys* of a certain precision to the corresponding vertex identifiers. + + Parameters + ---------- + precision : int, optional + Precision for converting numbers to strings. + Default is :attr:`TOL.precision`. + + Returns + ------- + dict[str, int] + A dictionary of geometric key-vertex pairs. + + See Also + -------- + :meth:`vertex_gkey` + + """ + gkey = TOL.geometric_key + xyz = self.vertex_coordinates + return {gkey(xyz(vertex), precision): vertex for vertex in self.vertices()} + + # -------------------------------------------------------------------------- + # Builders + # -------------------------------------------------------------------------- + + def add_vertex(self, key=None, attr_dict=None, **kwattr): + """Add a vertex and specify its attributes. + + Parameters + ---------- + key : int, optional + The identifier of the vertex. + Defaults to None. + attr_dict : dict, optional + A dictionary of vertex attributes. + Defaults to None. + **kwattr : dict, optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + int + The identifier of the vertex. + + See Also + -------- + :meth:`add_face`, :meth:`add_cell`, :meth:`add_edge` + + """ + if key is None: + key = self._max_vertex = self._max_vertex + 1 + key = int(key) + if key > self._max_vertex: + self._max_vertex = key + + if key not in self._vertex: + self._vertex[key] = {} + self._edge[key] = {} + self._plane[key] = {} + + attr = attr_dict or {} + attr.update(kwattr) + self._vertex[key].update(attr) + + return key + + def add_edge(self, u, v, attr_dict=None, **kwattr): + """Add an edge and specify its attributes. + + Parameters + ---------- + u : int + The identifier of the first node of the edge. + v : int + The identifier of the second node of the edge. + attr_dict : dict[str, Any], optional + A dictionary of edge attributes. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + tuple[int, int] + The identifier of the edge. + + Raises + ------ + ValueError + If either of the vertices of the edge does not exist. + + Notes + ----- + Edges can be added independently from faces or cells. + However, whenever a face is added all edges of that face are added as well. + + """ + if u not in self._vertex: + raise ValueError("Cannot add edge {}, {} has no vertex {}".format((u, v), self.name, u)) + if v not in self._vertex: + raise ValueError("Cannot add edge {}, {} has no vertex {}".format((u, v), self.name, u)) + + attr = attr_dict or {} + attr.update(kwattr) + + uv = tuple(sorted((u, v))) + + data = self._edge_data.get(uv, {}) + data.update(attr) + 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? + # 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._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 + + def add_face(self, vertices, fkey=None, attr_dict=None, **kwattr): + """Add a face to the cell network. + + Parameters + ---------- + vertices : list[int] + A list of ordered vertex keys representing the face. + For every vertex that does not yet exist, a new vertex is created. + fkey : int, optional + The face identifier. + attr_dict : dict[str, Any], optional + dictionary of halfface attributes. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + int + The key of the face. + + See Also + -------- + :meth:`add_vertex`, :meth:`add_cell`, :meth:`add_edge` + + Notes + ----- + If no key is provided for the face, one is generated + automatically. An automatically generated key is an integer that increments + the highest integer value of any key used so far by 1. + + If a key with an integer value is provided that is higher than the current + highest integer key value, then the highest integer value is updated accordingly. + + All edges of the faces are automatically added if they don't exsit yet. + The vertices of the face should form a continuous closed loop. + However, the cycle direction doesn't matter. + + """ + if len(vertices) < 3: + return + + 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 + + self._face[fkey] = vertices + + attr = attr_dict or {} + attr.update(kwattr) + for name, value in attr.items(): + self.face_attribute(fkey, name, value) + + for u, v in pairwise(vertices + vertices[:1]): + 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.add_edge(u, v) + + return fkey + + def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): + """Add a cell to the cell network object. + + In order to add a valid cell to the network, the faces must form a closed mesh. + If the faces do not form a closed mesh, the cell is not added to the network. + + Parameters + ---------- + faces : list[int] + The face keys of the cell. + ckey : int, optional + The cell identifier. + attr_dict : dict[str, Any], optional + A dictionary of cell attributes. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + int + The key of the cell. + + Raises + ------ + ValueError + If something is wrong with the passed faces. + TypeError + If the provided cell key is not an integer. + + Notes + ----- + If no key is provided for the cell, one is generated + automatically. An automatically generated key is an integer that increments + the highest integer value of any key used so far by 1. + + If a key with an integer value is provided that is higher than the current + 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 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: + raise ValueError("Cannot add cell, faces {} can not be unified.".format(faces)) + + # 3. Check if the faces are oriented correctly + # 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: + mesh.flip_cycles() + + if ckey is None: + ckey = self._max_cell = self._max_cell + 1 + ckey = int(ckey) + if ckey > self._max_cell: + self._max_cell = ckey + + self._cell[ckey] = {} + + attr = attr_dict or {} + attr.update(kwattr) + for name, value in attr.items(): + self.cell_attribute(ckey, name, value) + + for fkey in mesh.faces(): + vertices = mesh.face_vertices(fkey) + for u, v in pairwise(vertices + vertices[:1]): + if u not in self._cell[ckey]: + self._cell[ckey][u] = {} + self._plane[u][v][fkey] = ckey + self._cell[ckey][u][v] = fkey + + return ckey + + # -------------------------------------------------------------------------- + # Modifiers + # -------------------------------------------------------------------------- + + # def delete_vertex(self, vertex): + # """Delete a vertex from the cell network and everything that is attached to it. + + # Parameters + # ---------- + # vertex : int + # The identifier of the vertex. + + # Returns + # ------- + # None + + # See Also + # -------- + # :meth:`delete_halfface`, :meth:`delete_cell` + + # """ + # for cell in self.vertex_cells(vertex): + # self.delete_cell(cell) + + def delete_face(self, face): + vertices = self.face_vertices(face) + # check first + for u, v in pairwise(vertices + vertices[:1]): + if self._plane[u][v][face] != None: + print("Cannot delete face %d, delete cell %s first" % (face, self._plane[u][v][face])) + return + if self._plane[v][u][face] != None: + print("Cannot delete face %d, delete cell %s first" % (face, self._plane[u][v][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[key] + + def delete_cell(self, cell): + # 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 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` + + # """ + # 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 remove_unused_vertices(self): + # """Remove all unused vertices from the cell network object. + + # Returns + # ------- + # None + + # """ + # for vertex in list(self.vertices()): + # if vertex not in self._plane: + # del self._vertex[vertex] + # else: + # if not self._plane[vertex]: + # del self._vertex[vertex] + # del self._plane[vertex] + + # -------------------------------------------------------------------------- + # Constructors + # -------------------------------------------------------------------------- + + # @classmethod + # def from_meshgrid(cls, dx=10, dy=None, dz=None, nx=10, ny=None, nz=None): + # """Construct a cell network from a 3D meshgrid. + + # Parameters + # ---------- + # dx : float, optional + # The size of the grid in the x direction. + # dy : float, optional + # The size of the grid in the y direction. + # Defaults to the value of `dx`. + # dz : float, optional + # The size of the grid in the z direction. + # Defaults to the value of `dx`. + # nx : int, optional + # The number of elements in the x direction. + # ny : int, optional + # The number of elements in the y direction. + # Defaults to the value of `nx`. + # nz : int, optional + # The number of elements in the z direction. + # Defaults to the value of `nx`. + + # Returns + # ------- + # :class:`compas.datastructures.VolMesh` + + # See Also + # -------- + # :meth:`from_obj`, :meth:`from_vertices_and_cells` + + # """ + # dy = dy or dx + # dz = dz or dx + # ny = ny or nx + # nz = nz or nx + + # vertices = [ + # [x, y, z] + # for z, x, y in product( + # linspace(0, dz, nz + 1), + # linspace(0, dx, nx + 1), + # linspace(0, dy, ny + 1), + # ) + # ] + # cells = [] + # for k, i, j in product(range(nz), range(nx), range(ny)): + # a = k * ((nx + 1) * (ny + 1)) + i * (ny + 1) + j + # b = k * ((nx + 1) * (ny + 1)) + (i + 1) * (ny + 1) + j + # c = k * ((nx + 1) * (ny + 1)) + (i + 1) * (ny + 1) + j + 1 + # d = k * ((nx + 1) * (ny + 1)) + i * (ny + 1) + j + 1 + # aa = (k + 1) * ((nx + 1) * (ny + 1)) + i * (ny + 1) + j + # bb = (k + 1) * ((nx + 1) * (ny + 1)) + (i + 1) * (ny + 1) + j + # cc = (k + 1) * ((nx + 1) * (ny + 1)) + (i + 1) * (ny + 1) + j + 1 + # dd = (k + 1) * ((nx + 1) * (ny + 1)) + i * (ny + 1) + j + 1 + # bottom = [d, c, b, a] + # front = [a, b, bb, aa] + # right = [b, c, cc, bb] + # left = [a, aa, dd, d] + # back = [c, d, dd, cc] + # top = [aa, bb, cc, dd] + # cells.append([bottom, front, left, back, right, top]) + + # return cls.from_vertices_and_cells(vertices, cells) + + @classmethod + def from_obj(cls, filepath, precision=None): + """Construct a cell network object from the data described in an OBJ file. + + Parameters + ---------- + filepath : path string | file-like object | URL string + A path, a file-like object or a URL pointing to a file. + precision: str, optional + The precision of the geometric map that is used to connect the lines. + + Returns + ------- + :class:`compas.datastructures.VolMesh` + A cell network object. + + See Also + -------- + :meth:`to_obj` + :meth:`from_meshgrid`, :meth:`from_vertices_and_cells` + :class:`compas.files.OBJ` + + """ + 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) + + @classmethod + def from_vertices_and_cells(cls, vertices, cells): + """Construct a cell network object from vertices and cells. + + Parameters + ---------- + vertices : list[list[float]] + Ordered list of vertices, represented by their XYZ coordinates. + cells : list[list[list[int]]] + List of cells defined by their faces. + + Returns + ------- + :class:`compas.datastructures.VolMesh` + A cell network object. + + See Also + -------- + :meth:`to_vertices_and_cells` + :meth:`from_obj` + + """ + cellnetwork = cls() + for x, y, z in vertices: + cellnetwork.add_vertex(x=x, y=y, z=z) + for cell in cells: + faces = [] + for vertices in cell: + face = cellnetwork.add_face(vertices) + faces.append(face) + cellnetwork.add_cell(faces) + return cellnetwork + + # -------------------------------------------------------------------------- + # Conversions + # -------------------------------------------------------------------------- + + # def to_obj(self, filepath, precision=None, **kwargs): + # """Write the cell network to an OBJ file. + + # Parameters + # ---------- + # filepath : path string | file-like object + # A path or a file-like object pointing to a file. + # precision: str, optional + # The precision of the geometric map that is used to connect the lines. + # unweld : bool, optional + # If True, all faces have their own unique vertices. + # If False, vertices are shared between faces if this is also the case in the mesh. + # Default is False. + + # Returns + # ------- + # None + + # See Also + # -------- + # :meth:`from_obj` + + # Warnings + # -------- + # This function only writes geometric data about the vertices and + # the faces to the file. + + # """ + # obj = OBJ(filepath, precision=precision) + # obj.write(self, **kwargs) + + # def to_vertices_and_cells(self): + # """Return the vertices and cells of a cell network. + + # Returns + # ------- + # list[list[float]] + # A list of vertices, represented by their XYZ coordinates. + # list[list[list[int]]] + # A list of cells, with each cell a list of faces, and each face a list of vertex indices. + + # See Also + # -------- + # :meth:`from_vertices_and_cells` + + # """ + # vertex_index = self.vertex_index() + # vertices = [self.vertex_coordinates(vertex) for vertex in self.vertices()] + # cells = [] + # for cell in self.cells(): + # faces = [ + # [vertex_index[vertex] for vertex in self.halfface_vertices(face)] for face in self.cell_faces(cell) + # ] + # cells.append(faces) + # return vertices, cells + + def edges_to_graph(self): + """Convert the edges of the cell network to a graph. + + Returns + ------- + :class:`compas.datastructures.Graph` + A graph object. + + """ + graph = Graph() + for vertex, attr in self.vertices(data=True): + x, y, z = self.vertex_coordinates(vertex) + graph.add_node(key=vertex, x=x, y=y, z=z, attr_dict=attr) + for (u, v), attr in self.edges(data=True): + graph.add_edge(u, v, attr_dict=attr) + return graph + + def cell_to_vertices_and_faces(self, cell): + """Return the vertices and faces of a cell. + + Parameters + ---------- + cell : int + Identifier of the cell. + + Returns + ------- + list[list[float]] + A list of vertices, represented by their XYZ coordinates, + list[list[int]] + A list of faces, with each face a list of vertex indices. + + See Also + -------- + :meth:`cell_to_mesh` + + """ + vertices = self.cell_vertices(cell) + faces = self.cell_faces(cell) + vertex_index = {vertex: index for index, vertex in enumerate(vertices)} + vertices = [self.vertex_coordinates(vertex) for vertex in vertices] + faces = [] + for face in self.cell_faces(cell): + faces.append([vertex_index[vertex] for vertex in self.cell_face_vertices(cell, face)]) + return vertices, faces + + def cell_to_mesh(self, cell): + """Construct a mesh object from from a cell of a cell network. + + Parameters + ---------- + cell : int + Identifier of the cell. + + Returns + ------- + :class:`compas.datastructures.Mesh` + A mesh object. + + See Also + -------- + :meth:`cell_to_vertices_and_faces` + + """ + vertices, faces = self.cell_to_vertices_and_faces(cell) + return Mesh.from_vertices_and_faces(vertices, faces) + + def faces_to_mesh(self, faces, data=False): + """Construct a mesh from a list of faces. + + Parameters + ---------- + faces : list + A list of face identifiers. + + Returns + ------- + :class:`compas.datastructures.Mesh` + A mesh. + + """ + faces_vertices = [self.face_vertices(face) for face in faces] + mesh = Mesh() + for fkey, vertices in zip(faces, faces_vertices): + for v in vertices: + x, y, z = self.vertex_coordinates(v) + mesh.add_vertex(key=v, x=x, y=y, z=z) + if data: + mesh.add_face(vertices, fkey=fkey, attr_dict=self.face_attributes(fkey)) + else: + mesh.add_face(vertices, fkey=fkey) + return mesh + + # -------------------------------------------------------------------------- + # General + # -------------------------------------------------------------------------- + + def centroid(self): + """Compute the centroid of the cell network. + + Returns + ------- + :class:`compas.geometry.Point` + The point at the centroid. + + """ + return Point(*centroid_points([self.vertex_coordinates(vertex) for vertex in self.vertices()])) + + def aabb(self): + """Calculate the axis aligned bounding box of the mesh. + + Returns + ------- + list[[float, float, float]] + XYZ coordinates of 8 points defining a box. + + """ + xyz = self.vertices_attributes("xyz") + return bounding_box(xyz) + + def number_of_vertices(self): + """Count the number of vertices in the cell network. + + Returns + ------- + int + The number of vertices. + + See Also + -------- + :meth:`number_of_edges`, :meth:`number_of_faces`, :meth:`number_of_cells` + + """ + return len(list(self.vertices())) + + def number_of_edges(self): + """Count the number of edges in the cell network. + + Returns + ------- + int + The number of edges. + + See Also + -------- + :meth:`number_of_vertices`, :meth:`number_of_faces`, :meth:`number_of_cells` + + """ + return len(list(self.edges())) + + def number_of_faces(self): + """Count the number of faces in the cell network. + + Returns + ------- + int + The number of faces. + + See Also + -------- + :meth:`number_of_vertices`, :meth:`number_of_edges`, :meth:`number_of_cells` + + """ + return len(list(self.faces())) + + def number_of_cells(self): + """Count the number of faces in the cell network. + + Returns + ------- + int + The number of cells. + + See Also + -------- + :meth:`number_of_vertices`, :meth:`number_of_edges`, :meth:`number_of_faces` + + """ + return len(list(self.cells())) + + def is_valid(self): + """Verify that the cell network is valid. + + Returns + ------- + bool + True if the cell network is valid. + False otherwise. + + """ + raise NotImplementedError + + # -------------------------------------------------------------------------- + # Vertex Accessors + # -------------------------------------------------------------------------- + + def vertices(self, data=False): + """Iterate over the vertices of the cell network. + + Parameters + ---------- + data : bool, optional + If True, yield the vertex attributes in addition to the vertex identifiers. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next vertex identifier. + If `data` is True, the next vertex as a (vertex, attr) a tuple. + + See Also + -------- + :meth:`edges`, :meth:`faces`, :meth:`cells` + + """ + for vertex in self._vertex: + if not data: + yield vertex + else: + yield vertex, self.vertex_attributes(vertex) + + def vertices_where(self, conditions=None, data=False, **kwargs): + """Get vertices for which a certain condition or set of conditions is true. + + Parameters + ---------- + conditions : dict, optional + A set of conditions in the form of key-value pairs. + The keys should be attribute names. The values can be attribute + values or ranges of attribute values in the form of min/max pairs. + data : bool, optional + If True, yield the vertex attributes in addition to the identifiers. + **kwargs : dict[str, Any], optional + Additional conditions provided as named function arguments. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next vertex that matches the condition. + If `data` is True, the next vertex and its attributes. + + See Also + -------- + :meth:`vertices_where_predicate` + :meth:`edges_where`, :meth:`faces_where`, :meth:`cells_where` + + """ + conditions = conditions or {} + conditions.update(kwargs) + + for key, attr in self.vertices(True): + is_match = True + + attr = attr or {} + + for name, value in conditions.items(): + method = getattr(self, name, None) + + if callable(method): + val = method(key) + + if isinstance(val, list): + if value not in val: + is_match = False + break + break + + if isinstance(value, (tuple, list)): + minval, maxval = value + if val < minval or val > maxval: + is_match = False + break + else: + if value != val: + is_match = False + break + + else: + if name not in attr: + is_match = False + break + + if isinstance(attr[name], list): + if value not in attr[name]: + is_match = False + break + break + + if isinstance(value, (tuple, list)): + minval, maxval = value + if attr[name] < minval or attr[name] > maxval: + is_match = False + break + else: + if value != attr[name]: + is_match = False + break + + if is_match: + if data: + yield key, attr + else: + yield key + + def vertices_where_predicate(self, predicate, data=False): + """Get vertices for which a certain condition or set of conditions is true using a lambda function. + + Parameters + ---------- + predicate : callable + The condition you want to evaluate. + The callable takes 2 parameters: the vertex identifier and the vertex attributes, and should return True or False. + data : bool, optional + If True, yield the vertex attributes in addition to the identifiers. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next vertex that matches the condition. + If `data` is True, the next vertex and its attributes. + + See Also + -------- + :meth:`vertices_where` + :meth:`edges_where_predicate`, :meth:`faces_where_predicate`, :meth:`cells_where_predicate` + + """ + for key, attr in self.vertices(True): + if predicate(key, attr): + if data: + yield key, attr + else: + yield key + + # -------------------------------------------------------------------------- + # Vertex Attributes + # -------------------------------------------------------------------------- + + def update_default_vertex_attributes(self, attr_dict=None, **kwattr): + """Update the default vertex attributes. + + Parameters + ---------- + attr_dict : dict[str, Any], optional + A dictionary of attributes with their default values. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + None + + See Also + -------- + :meth:`update_default_edge_attributes`, :meth:`update_default_face_attributes`, :meth:`update_default_cell_attributes` + + Notes + ----- + Named arguments overwrite correpsonding name-value pairs in the attribute dictionary. + + """ + if not attr_dict: + attr_dict = {} + attr_dict.update(kwattr) + self.default_vertex_attributes.update(attr_dict) + + def vertex_attribute(self, vertex, name, value=None): + """Get or set an attribute of a vertex. + + Parameters + ---------- + vertex : int + The vertex identifier. + name : str + The name of the attribute + value : object, optional + The value of the attribute. + + Returns + ------- + object | None + The value of the attribute, + or None when the function is used as a "setter". + + Raises + ------ + KeyError + If the vertex does not exist. + + See Also + -------- + :meth:`unset_vertex_attribute` + :meth:`vertex_attributes`, :meth:`vertices_attribute`, :meth:`vertices_attributes` + :meth:`edge_attribute`, :meth:`face_attribute`, :meth:`cell_attribute` + + """ + if vertex not in self._vertex: + raise KeyError(vertex) + if value is not None: + self._vertex[vertex][name] = value + return None + if name in self._vertex[vertex]: + return self._vertex[vertex][name] + else: + if name in self.default_vertex_attributes: + return self.default_vertex_attributes[name] + + def unset_vertex_attribute(self, vertex, name): + """Unset the attribute of a vertex. + + Parameters + ---------- + vertex : int + The vertex identifier. + name : str + The name of the attribute. + + Returns + ------- + None + + Raises + ------ + KeyError + If the vertex does not exist. + + See Also + -------- + :meth:`vertex_attribute` + + Notes + ----- + Unsetting the value of a vertex attribute implicitly sets it back to the value + stored in the default vertex attribute dict. + + """ + if name in self._vertex[vertex]: + del self._vertex[vertex][name] + + def vertex_attributes(self, vertex, names=None, values=None): + """Get or set multiple attributes of a vertex. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + names : list[str], optional + A list of attribute names. + values : list[Any], optional + A list of attribute values. + + Returns + ------- + dict[str, Any] | list[Any] | None + If the parameter `names` is empty, + the function returns a dictionary of all attribute name-value pairs of the vertex. + If the parameter `names` is not empty, + the function returns a list of the values corresponding to the requested attribute names. + The function returns None if it is used as a "setter". + + Raises + ------ + KeyError + If the vertex does not exist. + + See Also + -------- + :meth:`vertex_attribute`, :meth:`vertices_attribute`, :meth:`vertices_attributes` + :meth:`edge_attributes`, :meth:`face_attributes`, :meth:`cell_attributes` + + """ + if vertex not in self._vertex: + raise KeyError(vertex) + if names and values is not None: + # use it as a setter + for name, value in zip(names, values): + self._vertex[vertex][name] = value + return + # use it as a getter + if not names: + # return all vertex attributes as a dict + return VertexAttributeView(self.default_vertex_attributes, self._vertex[vertex]) + values = [] + for name in names: + if name in self._vertex[vertex]: + values.append(self._vertex[vertex][name]) + elif name in self.default_vertex_attributes: + values.append(self.default_vertex_attributes[name]) + else: + values.append(None) + return values + + def vertices_attribute(self, name, value=None, keys=None): + """Get or set an attribute of multiple vertices. + + Parameters + ---------- + name : str + The name of the attribute. + value : object, optional + The value of the attribute. + Default is None. + keys : list[int], optional + A list of vertex identifiers. + + Returns + ------- + list[Any] | None + The value of the attribute for each vertex, + or None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the vertices does not exist. + + See Also + -------- + :meth:`vertex_attribute`, :meth:`vertex_attributes`, :meth:`vertices_attributes` + :meth:`edges_attribute`, :meth:`faces_attribute`, :meth:`cells_attribute` + + """ + vertices = keys or self.vertices() + if value is not None: + for vertex in vertices: + self.vertex_attribute(vertex, name, value) + return + return [self.vertex_attribute(vertex, name) for vertex in vertices] + + def vertices_attributes(self, names=None, values=None, keys=None): + """Get or set multiple attributes of multiple vertices. + + Parameters + ---------- + names : list[str], optional + The names of the attribute. + Default is None. + values : list[Any], optional + The values of the attributes. + Default is None. + key : list[Any], optional + A list of vertex identifiers. + + Returns + ------- + list[dict[str, Any]] | list[list[Any]] | None + If the parameter `names` is empty, + the function returns a list containing an attribute dict per vertex. + If the parameter `names` is not empty, + the function returns a list containing a list of attribute values per vertex corresponding to the provided attribute names. + The function returns None if it is used as a "setter". + + Raises + ------ + KeyError + If any of the vertices does not exist. + + See Also + -------- + :meth:`vertex_attribute`, :meth:`vertex_attributes`, :meth:`vertices_attribute` + :meth:`edges_attributes`, :meth:`faces_attributes`, :meth:`cells_attributes` + + """ + vertices = keys or self.vertices() + if values: + for vertex in vertices: + self.vertex_attributes(vertex, names, values) + return + return [self.vertex_attributes(vertex, names) for vertex in vertices] + + # -------------------------------------------------------------------------- + # Vertex Topology + # -------------------------------------------------------------------------- + + def has_vertex(self, vertex): + """Verify that a vertex is in the cell network. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + + Returns + ------- + bool + True if the vertex is in the cell network. + False otherwise. + + See Also + -------- + :meth:`has_edge`, :meth:`has_face`, :meth:`has_cell` + + """ + return vertex in self._vertex + + def vertex_neighbors(self, vertex): + """Return the vertex neighbors of a vertex. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + + Returns + ------- + list[int] + The list of neighboring vertices. + + See Also + -------- + :meth:`vertex_degree`, :meth:`vertex_min_degree`, :meth:`vertex_max_degree` + :meth:`vertex_faces`, :meth:`vertex_halffaces`, :meth:`vertex_cells` + :meth:`vertex_neighborhood` + + """ + return self._edge[vertex].keys() + + def vertex_neighborhood(self, vertex, ring=1): + """Return the vertices in the neighborhood of a vertex. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + ring : int, optional + The number of neighborhood rings to include. + + Returns + ------- + list[int] + The vertices in the neighborhood. + + See Also + -------- + :meth:`vertex_neighbors` + + Notes + ----- + The vertices in the neighborhood are unordered. + + """ + nbrs = set(self.vertex_neighbors(vertex)) + i = 1 + while True: + if i == ring: + break + temp = [] + for nbr in nbrs: + temp += self.vertex_neighbors(nbr) + nbrs.update(temp) + i += 1 + return list(nbrs - set([vertex])) + + def vertex_degree(self, vertex): + """Count the neighbors of a vertex. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + + Returns + ------- + int + The degree of the vertex. + + See Also + -------- + :meth:`vertex_neighbors`, :meth:`vertex_min_degree`, :meth:`vertex_max_degree` + + """ + return len(self.vertex_neighbors(vertex)) + + def vertex_min_degree(self): + """Compute the minimum degree of all vertices. + + Returns + ------- + int + The lowest degree of all vertices. + + See Also + -------- + :meth:`vertex_degree`, :meth:`vertex_max_degree` + + """ + if not self._vertex: + return 0 + return min(self.vertex_degree(vertex) for vertex in self.vertices()) + + def vertex_max_degree(self): + """Compute the maximum degree of all vertices. + + Returns + ------- + int + The highest degree of all vertices. + + See Also + -------- + :meth:`vertex_degree`, :meth:`vertex_min_degree` + + """ + if not self._vertex: + return 0 + return max(self.vertex_degree(vertex) for vertex in self.vertices()) + + def vertex_faces(self, vertex): + """Return all faces connected to a vertex. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + + Returns + ------- + list[int] + The list of faces connected to a vertex. + + See Also + -------- + :meth:`vertex_neighbors`, :meth:`vertex_cells` + + """ + faces = [] + for nbr in self._plane[vertex]: + for face in self._plane[vertex][nbr]: + if face is not None: + faces.append(face) + return faces + + def vertex_cells(self, vertex): + """Return all cells connected to a vertex. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + + Returns + ------- + list[int] + The list of cells connected to a vertex. + + See Also + -------- + :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(): + if cell is not None: + cells.add(cell) + return list(cells) + + # def is_vertex_on_boundary(self, vertex): + # """Verify that a vertex is on a boundary. + + # Parameters + # ---------- + # vertex : int + # The identifier of the vertex. + + # Returns + # ------- + # bool + # True if the vertex is on the boundary. + # False otherwise. + + # See Also + # -------- + # :meth:`is_edge_on_boundary`, :meth:`is_face_on_boundary`, :meth:`is_cell_on_boundary` + + # """ + # halffaces = self.vertex_halffaces(vertex) + # for halfface in halffaces: + # if self.is_halfface_on_boundary(halfface): + # return True + # return False + + # -------------------------------------------------------------------------- + # Vertex Geometry + # -------------------------------------------------------------------------- + + def vertex_coordinates(self, vertex, axes="xyz"): + """Return the coordinates of a vertex. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + axes : str, optional + The axes alon which to take the coordinates. + Should be a combination of x, y, and z. + + Returns + ------- + list[float] + Coordinates of the vertex. + """ + return [self._vertex[vertex][axis] for axis in axes] + + def vertices_coordinates(self, vertices, axes="xyz"): + """Return the coordinates of multiple vertices. + + Parameters + ---------- + vertices : list of int + The vertex identifiers. + axes : str, optional + The axes alon which to take the coordinates. + Should be a combination of x, y, and z. + + Returns + ------- + list of list[float] + Coordinates of the vertices. + """ + return [self.vertex_coordinates(vertex, axes=axes) for vertex in vertices] + + def vertex_point(self, vertex): + """Return the point representation of a vertex. + + Parameters + ---------- + vertex : int + The identifier of the vertex. + + Returns + ------- + :class:`compas.geometry.Point` + The point. + """ + return Point(*self.vertex_coordinates(vertex)) + + def vertices_points(self, vertices): + """Returns the point representation of multiple vertices. + + Parameters + ---------- + vertices : list of int + The vertex identifiers. + + Returns + ------- + list of :class:`compas.geometry.Point` + The points. + """ + return [self.vertex_point(vertex) for vertex in vertices] + + # -------------------------------------------------------------------------- + # Edge Accessors + # -------------------------------------------------------------------------- + + def edges(self, data=False): + """Iterate over the edges of the cell network. + + Parameters + ---------- + data : bool, optional + If True, yield the edge attributes in addition to the edge identifiers. + + Yields + ------ + tuple[int, int] | tuple[tuple[int, int], dict[str, Any]] + If `data` is False, the next edge identifier (u, v). + If `data` is True, the next edge identifier and its attributes as a ((u, v), attr) tuple. + + """ + seen = set() + for u, nbrs in iter(self._edge.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 + + def edges_where(self, conditions=None, data=False, **kwargs): + """Get edges for which a certain condition or set of conditions is true. + + Parameters + ---------- + conditions : dict, optional + A set of conditions in the form of key-value pairs. + The keys should be attribute names. The values can be attribute + values or ranges of attribute values in the form of min/max pairs. + data : bool, optional + If True, yield the edge attributes in addition to the identifiers. + **kwargs : dict[str, Any], optional + Additional conditions provided as named function arguments. + + Yields + ------ + tuple[int, int] | tuple[tuple[int, int], dict[str, Any]] + If `data` is False, the next edge as a (u, v) tuple. + If `data` is True, the next edge as a (u, v, data) tuple. + + See Also + -------- + :meth:`edges_where_predicate` + :meth:`vertices_where`, :meth:`faces_where`, :meth:`cells_where` + + """ + conditions = conditions or {} + conditions.update(kwargs) + + for key in self.edges(): + is_match = True + + attr = self.edge_attributes(key) or {} + + for name, value in conditions.items(): + method = getattr(self, name, None) + + if method and callable(method): + val = method(key) + elif name in attr: + val = attr[name] + else: + is_match = False + break + + if isinstance(val, list): + if value not in val: + is_match = False + break + elif isinstance(value, (tuple, list)): + minval, maxval = value + if val < minval or val > maxval: + is_match = False + break + else: + if value != val: + is_match = False + break + + if is_match: + if data: + yield key, attr + else: + yield key + + def edges_where_predicate(self, predicate, data=False): + """Get edges for which a certain condition or set of conditions is true using a lambda function. + + Parameters + ---------- + predicate : callable + The condition you want to evaluate. + The callable takes 2 parameters: the edge identifier and the edge attributes, and should return True or False. + data : bool, optional + If True, yield the edge attributes in addition to the identifiers. + + Yields + ------ + tuple[int, int] | tuple[tuple[int, int], dict[str, Any]] + If `data` is False, the next edge as a (u, v) tuple. + If `data` is True, the next edge as a (u, v, data) tuple. + + See Also + -------- + :meth:`edges_where` + :meth:`vertices_where_predicate`, :meth:`faces_where_predicate`, :meth:`cells_where_predicate` + + """ + for key, attr in self.edges(True): + if predicate(key, attr): + if data: + yield key, attr + else: + yield key + + # -------------------------------------------------------------------------- + # Edge Attributes + # -------------------------------------------------------------------------- + + def update_default_edge_attributes(self, attr_dict=None, **kwattr): + """Update the default edge attributes. + + Parameters + ---------- + attr_dict : dict[str, Any], optional + A dictionary of attributes with their default values. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + None + + See Also + -------- + :meth:`update_default_vertex_attributes`, :meth:`update_default_face_attributes`, :meth:`update_default_cell_attributes` + + Notes + ----- + Named arguments overwrite correpsonding key-value pairs in the attribute dictionary. + + """ + if not attr_dict: + attr_dict = {} + attr_dict.update(kwattr) + self.default_edge_attributes.update(attr_dict) + + def edge_attribute(self, edge, name, value=None): + """Get or set an attribute of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + name : str + The name of the attribute. + value : object, optional + The value of the attribute. + + Returns + ------- + object | None + The value of the attribute, or None when the function is used as a "setter". + + Raises + ------ + KeyError + If the edge does not exist. + """ + if not self.has_edge(edge): + raise KeyError(edge) + + attr = self._edge_data.get(tuple(sorted(edge)), {}) + + if value is not None: + attr.update({name: value}) + self._edge_data[tuple(sorted(edge))] = attr + return + if name in attr: + return attr[name] + if name in self.default_edge_attributes: + return self.default_edge_attributes[name] + + def unset_edge_attribute(self, edge, name): + """Unset the attribute of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + name : str + The name of the attribute. + + Raises + ------ + KeyError + If the edge does not exist. + + Returns + ------- + None + + See Also + -------- + :meth:`edge_attribute` + + Notes + ----- + Unsetting the value of an edge attribute implicitly sets it back to the value + stored in the default edge attribute dict. + + """ + if not self.has_edge(edge): + raise KeyError(edge) + + 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. + + Parameters + ---------- + edge : tuple[int, int] + The identifier of the edge. + names : list[str], optional + A list of attribute names. + values : list[Any], optional + A list of attribute values. + + Returns + ------- + dict[str, Any] | list[Any] | None + If the parameter `names` is empty, a dictionary of all attribute name-value pairs of the edge. + If the parameter `names` is not empty, a list of the values corresponding to the provided names. + None if the function is used as a "setter". + + Raises + ------ + KeyError + If the edge does not exist. + + See Also + -------- + :meth:`edge_attribute`, :meth:`edges_attribute`, :meth:`edges_attributes` + :meth:`vertex_attributes`, :meth:`face_attributes`, :meth:`cell_attributes` + + """ + if not self.has_edge(edge): + raise KeyError(edge) + + if names and values: + for name, value in zip(names, values): + self._edge_data[tuple(sorted(edge))][name] = value + return + if not names: + return EdgeAttributeView(self.default_edge_attributes, self._edge_data[tuple(sorted(edge))]) + values = [] + for name in names: + value = self.edge_attribute(edge, name) + values.append(value) + return values + + def edges_attribute(self, name, value=None, edges=None): + """Get or set an attribute of multiple edges. + + Parameters + ---------- + name : str + The name of the attribute. + value : object, optional + The value of the attribute. + Default is None. + edges : list[tuple[int, int]], optional + A list of edge identifiers. + + Returns + ------- + list[Any] | None + A list containing the value per edge of the requested attribute, + or None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the edges does not exist. + + See Also + -------- + :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attributes` + :meth:`vertex_attribute`, :meth:`face_attribute`, :meth:`cell_attribute` + + """ + edges = edges or self.edges() + if value is not None: + for edge in edges: + self.edge_attribute(edge, name, value) + return + return [self.edge_attribute(edge, name) for edge in edges] + + def edges_attributes(self, names=None, values=None, edges=None): + """Get or set multiple attributes of multiple edges. + + Parameters + ---------- + names : list[str], optional + The names of the attribute. + values : list[Any], optional + The values of the attributes. + edges : list[tuple[int, int]], optional + A list of edge identifiers. + + Returns + ------- + list[dict[str, Any]] | list[list[Any]] | None + If the parameter `names` is empty, + a list containing per edge an attribute dict with all attributes (default + custom) of the edge. + If the parameter `names` is not empty, + a list containing per edge a list of attribute values corresponding to the requested names. + None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the edges does not exist. + + See Also + -------- + :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute` + :meth:`vertex_attributes`, :meth:`face_attributes`, :meth:`cell_attributes` + + """ + edges = edges or self.edges() + if values: + for edge in edges: + self.edge_attributes(edge, names, values) + return + return [self.edge_attributes(edge, names) for edge in edges] + + # -------------------------------------------------------------------------- + # Edge Topology + # -------------------------------------------------------------------------- + + def has_edge(self, edge, directed=False): + """Verify that the cell network contains a directed edge (u, v). + + Parameters + ---------- + edge : tuple[int, int] + The identifier of the edge. + directed : bool, optional + If ``True``, the direction of the edge should be taken into account. + + Returns + ------- + bool + True if the edge exists. + False otherwise. + + See Also + -------- + :meth:`has_vertex`, :meth:`has_face`, :meth:`has_cell` + + """ + u, v = edge + if directed: + return u in self._edge and v in self._edge[u] + return (u in self._edge and v in self._edge[u]) or (v in self._edge and u in self._edge[v]) + + def edge_faces(self, edge): + """Return the faces adjacent to an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + + Returns + ------- + list[int] + The identifiers of the adjacent faces. + """ + u, v = edge + faces = set() + if v in self._plane[u]: + faces.update(self._plane[u][v].keys()) + if u in self._plane[v]: + faces.update(self._plane[v][u].keys()) + return sorted(list(faces)) + + def edge_cells(self, edge): + """Ordered cells around edge (u, v). + + Parameters + ---------- + edge : tuple[int, int] + The identifier of the edge. + + Returns + ------- + list[int] + Ordered list of keys identifying the ordered cells. + + See Also + -------- + :meth:`edge_halffaces` + + """ + # @Roman: should v, u also be checked? + u, v = edge + cells = [] + for cell in self._plane[u][v].values(): + if cell is not None: + cells.append(cell) + return cells + + # def is_edge_on_boundary(self, edge): + # """Verify that an edge is on the boundary. + + # Parameters + # ---------- + # edge : tuple[int, int] + # The identifier of the edge. + + # Returns + # ------- + # bool + # True if the edge is on the boundary. + # False otherwise. + + # See Also + # -------- + # :meth:`is_vertex_on_boundary`, :meth:`is_face_on_boundary`, :meth:`is_cell_on_boundary` + + # Notes + # ----- + # This method simply checks if u-v or v-u is on the edge of the cell network. + # The direction u-v does not matter. + + # """ + # u, v = edge + # return None in self._plane[u][v].values() + + def edges_without_face(self): + """Find the edges that are not part of a face. + + Returns + ------- + list[int] + The edges without face. + + """ + edges = {edge for edge in self.edges() if not self.edge_faces(edge)} + return list(edges) + + def nonmanifold_edges(self): + """Returns the edges that belong to more than two faces. + + Returns + ------- + list[int] + The edges without face. + + """ + edges = {edge for edge in self.edges() if len(self.edge_faces(edge)) > 2} + return list(edges) + + # -------------------------------------------------------------------------- + # Edge Geometry + # -------------------------------------------------------------------------- + + def edge_coordinates(self, edge, axes="xyz"): + """Return the coordinates of the start and end point of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + axes : str, optional + The axes along which the coordinates should be included. + + Returns + ------- + tuple[list[float], list[float]] + The coordinates of the start point. + The coordinates of the end point. + """ + u, v = edge + return self.vertex_coordinates(u, axes=axes), self.vertex_coordinates(v, axes=axes) + + def edge_start(self, edge): + """Return the start point of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + + Returns + ------- + :class:`compas.geometry.Point` + The start point. + """ + return self.vertex_point(edge[0]) + + def edge_end(self, edge): + """Return the end point of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + + Returns + ------- + :class:`compas.geometry.Point` + The end point. + """ + return self.vertex_point(edge[1]) + + def edge_midpoint(self, edge): + """Return the midpoint of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + + Returns + ------- + :class:`compas.geometry.Point` + The midpoint. + + See Also + -------- + :meth:`edge_start`, :meth:`edge_end`, :meth:`edge_point` + + """ + a, b = self.edge_coordinates(edge) + return Point(0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])) + + def edge_point(self, edge, t=0.5): + """Return the point at a parametric location along an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + t : float, optional + The location of the point on the edge. + If the value of `t` is outside the range 0-1, the point will + lie in the direction of the edge, but not on the edge vector. + + Returns + ------- + :class:`compas.geometry.Point` + The XYZ coordinates of the point. + + See Also + -------- + :meth:`edge_start`, :meth:`edge_end`, :meth:`edge_midpoint` + + """ + if t == 0: + return self.edge_start(edge) + if t == 1: + return self.edge_end(edge) + if t == 0.5: + return self.edge_midpoint(edge) + + a, b = self.edge_coordinates(edge) + ab = subtract_vectors(b, a) + return Point(*add_vectors(a, scale_vector(ab, t))) + + def edge_vector(self, edge): + """Return the vector of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + + Returns + ------- + :class:`compas.geometry.Vector` + The vector from start to end. + """ + a, b = self.edge_coordinates(edge) + return Vector.from_start_end(a, b) + + def edge_direction(self, edge): + """Return the direction vector of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + + Returns + ------- + :class:`compas.geometry.Vector` + The direction vector of the edge. + """ + return Vector(*normalize_vector(self.edge_vector(edge))) + + def edge_line(self, edge): + """Return the line representation of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + + Returns + ------- + :class:`compas.geometry.Line` + The line. + """ + return Line(*self.edge_coordinates(edge)) + + def edge_length(self, edge): + """Return the length of an edge. + + Parameters + ---------- + edge : tuple[int, int] + The edge identifier. + + Returns + ------- + float + The length of the edge. + """ + a, b = self.edge_coordinates(edge) + return distance_point_point(a, b) + + # -------------------------------------------------------------------------- + # Face Accessors + # -------------------------------------------------------------------------- + + def faces(self, data=False): + """Iterate over the halffaces of the cell network and yield faces. + + Parameters + ---------- + data : bool, optional + If True, yield the face attributes in addition to the face identifiers. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next face identifier. + If `data` is True, the next face as a (face, attr) tuple. + + See Also + -------- + :meth:`vertices`, :meth:`edges`, :meth:`cells` + + Notes + ----- + Volmesh faces have no topological meaning (analogous to an edge of a mesh). + They are typically used for geometric operations (i.e. planarisation). + Between the interface of two cells, there are two interior faces (one from each cell). + Only one of these two interior faces are returned as a "face". + The unique faces are found by comparing string versions of sorted vertex lists. + + """ + for face in self._face: + if not data: + yield face + else: + yield face, self.face_attributes(face) + + def faces_where(self, conditions=None, data=False, **kwargs): + """Get faces for which a certain condition or set of conditions is true. + + Parameters + ---------- + conditions : dict, optional + A set of conditions in the form of key-value pairs. + The keys should be attribute names. The values can be attribute + values or ranges of attribute values in the form of min/max pairs. + data : bool, optional + If True, yield the face attributes in addition to the identifiers. + **kwargs : dict[str, Any], optional + Additional conditions provided as named function arguments. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next face that matches the condition. + If `data` is True, the next face and its attributes. + + See Also + -------- + :meth:`faces_where_predicate` + :meth:`vertices_where`, :meth:`edges_where`, :meth:`cells_where` + + """ + conditions = conditions or {} + conditions.update(kwargs) + + for fkey in self.faces(): + is_match = True + + attr = self.face_attributes(fkey) or {} + + for name, value in conditions.items(): + method = getattr(self, name, None) + + if method and callable(method): + val = method(fkey) + elif name in attr: + val = attr[name] + else: + is_match = False + break + + if isinstance(val, list): + if value not in val: + is_match = False + break + elif isinstance(value, (tuple, list)): + minval, maxval = value + if val < minval or val > maxval: + is_match = False + break + else: + if value != val: + is_match = False + break + + if is_match: + if data: + yield fkey, attr + else: + yield fkey + + def faces_where_predicate(self, predicate, data=False): + """Get faces for which a certain condition or set of conditions is true using a lambda function. + + Parameters + ---------- + predicate : callable + The condition you want to evaluate. + The callable takes 2 parameters: the face identifier and the the face attributes, and should return True or False. + data : bool, optional + If True, yield the face attributes in addition to the identifiers. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next face that matches the condition. + If `data` is True, the next face and its attributes. + + See Also + -------- + :meth:`faces_where` + :meth:`vertices_where_predicate`, :meth:`edges_where_predicate`, :meth:`cells_where_predicate` + + """ + for fkey, attr in self.faces(True): + if predicate(fkey, attr): + if data: + yield fkey, attr + else: + yield fkey + + # -------------------------------------------------------------------------- + # Face Attributes + # -------------------------------------------------------------------------- + + def update_default_face_attributes(self, attr_dict=None, **kwattr): + """Update the default face attributes. + + Parameters + ---------- + attr_dict : dict[str, Any], optional + A dictionary of attributes with their default values. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + None + + See Also + -------- + :meth:`update_default_vertex_attributes`, :meth:`update_default_edge_attributes`, :meth:`update_default_cell_attributes` + + Notes + ----- + Named arguments overwrite correpsonding key-value pairs in the attribute dictionary. + + """ + if not attr_dict: + attr_dict = {} + attr_dict.update(kwattr) + self.default_face_attributes.update(attr_dict) + + def face_attribute(self, face, name, value=None): + """Get or set an attribute of a face. + + Parameters + ---------- + face : int + The face identifier. + name : str + The name of the attribute. + value : object, optional + The value of the attribute. + + Returns + ------- + object | None + The value of the attribute, or None when the function is used as a "setter". + + Raises + ------ + KeyError + If the face does not exist. + + See Also + -------- + :meth:`unset_face_attribute` + :meth:`face_attributes`, :meth:`faces_attribute`, :meth:`faces_attributes` + :meth:`vertex_attribute`, :meth:`edge_attribute`, :meth:`cell_attribute` + + """ + if face not in self._face: + raise KeyError(face) + + if value is not None: + if face not in self._face_data: + self._face_data[face] = {} + self._face_data[face][name] = value + return + if face in self._face_data and name in self._face_data[face]: + return self._face_data[face][name] + if name in self.default_face_attributes: + return self.default_face_attributes[name] + + def unset_face_attribute(self, face, name): + """Unset the attribute of a face. + + Parameters + ---------- + face : int + The face identifier. + name : str + The name of the attribute. + + Raises + ------ + KeyError + If the face does not exist. + + Returns + ------- + None + + See Also + -------- + :meth:`face_attribute` + + Notes + ----- + Unsetting the value of a face attribute implicitly sets it back to the value + stored in the default face attribute dict. + + """ + if face not in self._face: + raise KeyError(face) + + if face in self._face_data and name in self._face_data[face]: + del self._face_data[face][name] + + def face_attributes(self, face, names=None, values=None): + """Get or set multiple attributes of a face. + + Parameters + ---------- + face : int + The identifier of the face. + names : list[str], optional + A list of attribute names. + values : list[Any], optional + A list of attribute values. + + Returns + ------- + dict[str, Any] | list[Any] | None + If the parameter `names` is empty, a dictionary of all attribute name-value pairs of the face. + If the parameter `names` is not empty, a list of the values corresponding to the provided names. + None if the function is used as a "setter". + + Raises + ------ + KeyError + If the face does not exist. + + See Also + -------- + :meth:`face_attribute`, :meth:`faces_attribute`, :meth:`faces_attributes` + :meth:`vertex_attributes`, :meth:`edge_attributes`, :meth:`cell_attributes` + + """ + if face not in self._face: + raise KeyError(face) + + if names and values: + for name, value in zip(names, values): + if face not in self._face_data: + self._face_data[face] = {} + self._face_data[face][name] = value + return + + if not names: + return FaceAttributeView(self.default_face_attributes, self._face_data.setdefault(face, {})) + + values = [] + for name in names: + value = self.face_attribute(face, name) + values.append(value) + return values + + def faces_attribute(self, name, value=None, faces=None): + """Get or set an attribute of multiple faces. + + Parameters + ---------- + name : str + The name of the attribute. + value : object, optional + The value of the attribute. + Default is None. + faces : list[int], optional + A list of face identifiers. + + Returns + ------- + list[Any] | None + A list containing the value per face of the requested attribute, + or None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the faces does not exist. + + See Also + -------- + :meth:`face_attribute`, :meth:`face_attributes`, :meth:`faces_attributes` + :meth:`vertex_attribute`, :meth:`edge_attribute`, :meth:`cell_attribute` + + """ + faces = faces or self.faces() + if value is not None: + for face in faces: + self.face_attribute(face, name, value) + return + return [self.face_attribute(face, name) for face in faces] + + def faces_attributes(self, names=None, values=None, faces=None): + """Get or set multiple attributes of multiple faces. + + Parameters + ---------- + names : list[str], optional + The names of the attribute. + Default is None. + values : list[Any], optional + The values of the attributes. + Default is None. + faces : list[int], optional + A list of face identifiers. + + Returns + ------- + list[dict[str, Any]] | list[list[Any]] | None + If the parameter `names` is empty, + a list containing per face an attribute dict with all attributes (default + custom) of the face. + If the parameter `names` is not empty, + a list containing per face a list of attribute values corresponding to the requested names. + None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the faces does not exist. + + See Also + -------- + :meth:`face_attribute`, :meth:`face_attributes`, :meth:`faces_attribute` + :meth:`vertex_attributes`, :meth:`edge_attributes`, :meth:`cell_attributes` + + """ + faces = faces or self.faces() + if values: + for face in faces: + self.face_attributes(face, names, values) + return + return [self.face_attributes(face, names) for face in faces] + + # -------------------------------------------------------------------------- + # Face Topology + # -------------------------------------------------------------------------- + + def has_face(self, face): + """Verify that a face is part of the cell network. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + bool + True if the face exists. + False otherwise. + + See Also + -------- + :meth:`has_vertex`, :meth:`has_edge`, :meth:`has_cell` + + """ + return face in self._face + + def face_vertices(self, face): + """The vertices of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + list[int] + Ordered vertex identifiers. + + """ + return self._face[face] + + def face_edges(self, face): + """The edges of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + list[tuple[int, int]] + Ordered edge identifiers. + + """ + vertices = self.face_vertices(face) + edges = [] + for u, v in pairwise(vertices + vertices[:1]): + # if v in self._edge[u]: + # edges.append((u, v)) + edges.append((u, v)) + return edges + + def face_cells(self, face): + """Return the cells connected to a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + list[int] + The identifiers of the cells connected to the face. + + """ + u, v = self.face_vertices(face)[:2] + cells = [] + if v in self._plane[u]: + cell = self._plane[u][v][face] + if cell is not None: + cells.append(cell) + cell = self._plane[v][u][face] + if cell is not None: + cells.append(cell) + return cells + + def faces_without_cell(self): + """Find the faces that are not part of a cell. + + Returns + ------- + list[int] + The faces without cell. + + """ + faces = {fkey for fkey in self.faces() if not self.face_cells(fkey)} + 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. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + bool + True if the face is on the boundary. + False otherwise. + + """ + u, v = self.face_vertices(face)[:2] + cu = 1 if self._plane[u][v][face] is None else 0 + cv = 1 if self._plane[v][u][face] is None else 0 + return cu + cv == 1 + + def faces_on_boundaries(self): + """Find the faces that are on the boundary. + + Returns + ------- + list[int] + The faces on the boundary. + + """ + return [face for face in self.faces() if self.is_face_on_boundary(face)] + + # -------------------------------------------------------------------------- + # Face Geometry + # -------------------------------------------------------------------------- + + def face_coordinates(self, face, axes="xyz"): + """Compute the coordinates of the vertices of a face. + + Parameters + ---------- + face : int + The identifier of the face. + axes : str, optional + The axes alon which to take the coordinates. + Should be a combination of x, y, and z. + + Returns + ------- + list[list[float]] + The coordinates of the vertices of the face. + + See Also + -------- + :meth:`face_points`, :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_centroid`, :meth:`face_center` + :meth:`face_area`, :meth:`face_flatness`, :meth:`face_aspect_ratio` + + """ + return [self.vertex_coordinates(vertex, axes=axes) for vertex in self.face_vertices(face)] + + def face_points(self, face): + """Compute the points of the vertices of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + list[:class:`compas.geometry.Point`] + The points of the vertices of the face. + + See Also + -------- + :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_centroid`, :meth:`face_center` + + """ + return [self.vertex_point(vertex) for vertex in self.face_vertices(face)] + + def face_polygon(self, face): + """Compute the polygon of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + :class:`compas.geometry.Polygon` + The polygon of the face. + + See Also + -------- + :meth:`face_points`, :meth:`face_normal`, :meth:`face_centroid`, :meth:`face_center` + + """ + return Polygon(self.face_points(face)) + + def face_normal(self, face, unitized=True): + """Compute the oriented normal of a face. + + Parameters + ---------- + face : int + The identifier of the face. + unitized : bool, optional + If True, unitize the normal vector. + + Returns + ------- + :class:`compas.geometry.Vector` + The normal vector. + + See Also + -------- + :meth:`face_points`, :meth:`face_polygon`, :meth:`face_centroid`, :meth:`face_center` + + """ + return Vector(*normal_polygon(self.face_coordinates(face), unitized=unitized)) + + def face_centroid(self, face): + """Compute the point at the centroid of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + :class:`compas.geometry.Point` + The coordinates of the centroid. + + See Also + -------- + :meth:`face_points`, :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_center` + + """ + return Point(*centroid_points(self.face_coordinates(face))) + + def face_center(self, face): + """Compute the point at the center of mass of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + :class:`compas.geometry.Point` + The coordinates of the center of mass. + + See Also + -------- + :meth:`face_points`, :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_centroid` + + """ + return Point(*centroid_polygon(self.face_coordinates(face))) + + def face_plane(self, face): + return Plane(self.face_center(face), self.face_normal(face)) + + def face_area(self, face): + """Compute the oriented area of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + float + The non-oriented area of the face. + + See Also + -------- + :meth:`face_flatness`, :meth:`face_aspect_ratio` + + """ + return length_vector(self.face_normal(face, unitized=False)) + + def face_flatness(self, face, maxdev=0.02): + """Compute the flatness of a face. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + float + The flatness. + + See Also + -------- + :meth:`face_area`, :meth:`face_aspect_ratio` + + Notes + ----- + compas.geometry.mesh_flatness function currently only works for quadrilateral faces. + This function uses the distance between each face vertex and its projected point + on the best-fit plane of the face as the flatness metric. + + """ + deviation = 0 + polygon = self.face_coordinates(face) + plane = bestfit_plane(polygon) + for pt in polygon: + pt_proj = project_point_plane(pt, plane) + dev = distance_point_point(pt, pt_proj) + if dev > deviation: + deviation = dev + return deviation + + def face_aspect_ratio(self, face): + """Face aspect ratio as the ratio between the lengths of the maximum and minimum face edges. + + Parameters + ---------- + face : int + The identifier of the face. + + Returns + ------- + float + The aspect ratio. + + See Also + -------- + :meth:`face_area`, :meth:`face_flatness` + + References + ---------- + .. [1] Wikipedia. *Types of mesh*. + Available at: https://en.wikipedia.org/wiki/Types_of_mesh. + + """ + lengths = [self.edge_length(edge) for edge in self.face_edges(face)] + return max(lengths) / min(lengths) + + # -------------------------------------------------------------------------- + # Cell Accessors + # -------------------------------------------------------------------------- + + def cells(self, data=False): + """Iterate over the cells of the volmesh. + + Parameters + ---------- + data : bool, optional + If True, yield the cell attributes in addition to the cell identifiers. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next cell identifier. + If `data` is True, the next cell as a (cell, attr) tuple. + + See Also + -------- + :meth:`vertices`, :meth:`edges`, :meth:`faces` + + """ + for cell in self._cell: + if not data: + yield cell + else: + yield cell, self.cell_attributes(cell) + + def cells_where(self, conditions=None, data=False, **kwargs): + """Get cells for which a certain condition or set of conditions is true. + + Parameters + ---------- + conditions : dict, optional + A set of conditions in the form of key-value pairs. + The keys should be attribute names. The values can be attribute + values or ranges of attribute values in the form of min/max pairs. + data : bool, optional + If True, yield the cell attributes in addition to the identifiers. + **kwargs : dict[str, Any], optional + Additional conditions provided as named function arguments. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next cell that matches the condition. + If `data` is True, the next cell and its attributes. + + See Also + -------- + :meth:`cells_where_predicate` + :meth:`vertices_where`, :meth:`edges_where`, :meth:`faces_where` + + """ + conditions = conditions or {} + conditions.update(kwargs) + + for ckey in self.cells(): + is_match = True + + attr = self.cell_attributes(ckey) or {} + + for name, value in conditions.items(): + method = getattr(self, name, None) + + if method and callable(method): + val = method(ckey) + elif name in attr: + val = attr[name] + else: + is_match = False + break + + if isinstance(val, list): + if value not in val: + is_match = False + break + elif isinstance(value, (tuple, list)): + minval, maxval = value + if val < minval or val > maxval: + is_match = False + break + else: + if value != val: + is_match = False + break + + if is_match: + if data: + yield ckey, attr + else: + yield ckey + + def cells_where_predicate(self, predicate, data=False): + """Get cells for which a certain condition or set of conditions is true using a lambda function. + + Parameters + ---------- + predicate : callable + The condition you want to evaluate. + The callable takes 2 parameters: the cell identifier and the cell attributes, and should return True or False. + data : bool, optional + If True, yield the cell attributes in addition to the identifiers. + + Yields + ------ + int | tuple[int, dict[str, Any]] + If `data` is False, the next cell that matches the condition. + If `data` is True, the next cell and its attributes. + + See Also + -------- + :meth:`cells_where` + :meth:`vertices_where_predicate`, :meth:`edges_where_predicate`, :meth:`faces_where_predicate` + + """ + for ckey, attr in self.cells(True): + if predicate(ckey, attr): + if data: + yield ckey, attr + else: + yield ckey + + # -------------------------------------------------------------------------- + # Cell Attributes + # -------------------------------------------------------------------------- + + def update_default_cell_attributes(self, attr_dict=None, **kwattr): + """Update the default cell attributes. + + Parameters + ---------- + attr_dict : dict[str, Any], optional + A dictionary of attributes with their default values. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + None + + See Also + -------- + :meth:`update_default_vertex_attributes`, :meth:`update_default_edge_attributes`, :meth:`update_default_face_attributes` + + Notes + ----- + Named arguments overwrite corresponding cell-value pairs in the attribute dictionary. + + """ + if not attr_dict: + attr_dict = {} + attr_dict.update(kwattr) + self.default_cell_attributes.update(attr_dict) + + def cell_attribute(self, cell, name, value=None): + """Get or set an attribute of a cell. + + Parameters + ---------- + cell : int + The cell identifier. + name : str + The name of the attribute. + value : object, optional + The value of the attribute. + + Returns + ------- + object | None + The value of the attribute, or None when the function is used as a "setter". + + Raises + ------ + KeyError + If the cell does not exist. + + See Also + -------- + :meth:`unset_cell_attribute` + :meth:`cell_attributes`, :meth:`cells_attribute`, :meth:`cells_attributes` + :meth:`vertex_attribute`, :meth:`edge_attribute`, :meth:`face_attribute` + + """ + if cell not in self._cell: + raise KeyError(cell) + if value is not None: + if cell not in self._cell_data: + self._cell_data[cell] = {} + self._cell_data[cell][name] = value + return + if cell in self._cell_data and name in self._cell_data[cell]: + return self._cell_data[cell][name] + if name in self.default_cell_attributes: + return self.default_cell_attributes[name] + + def unset_cell_attribute(self, cell, name): + """Unset the attribute of a cell. + + Parameters + ---------- + cell : int + The cell identifier. + name : str + The name of the attribute. + + Returns + ------- + None + + Raises + ------ + KeyError + If the cell does not exist. + + See Also + -------- + :meth:`cell_attribute` + + Notes + ----- + Unsetting the value of a cell attribute implicitly sets it back to the value + stored in the default cell attribute dict. + + """ + if cell not in self._cell: + raise KeyError(cell) + if cell in self._cell_data: + if name in self._cell_data[cell]: + del self._cell_data[cell][name] + + def cell_attributes(self, cell, names=None, values=None): + """Get or set multiple attributes of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + names : list[str], optional + A list of attribute names. + values : list[Any], optional + A list of attribute values. + + Returns + ------- + dict[str, Any] | list[Any] | None + If the parameter `names` is empty, a dictionary of all attribute name-value pairs of the cell. + If the parameter `names` is not empty, a list of the values corresponding to the provided names. + None if the function is used as a "setter". + + Raises + ------ + KeyError + If the cell does not exist. + + See Also + -------- + :meth:`cell_attribute`, :meth:`cells_attribute`, :meth:`cells_attributes` + :meth:`vertex_attributes`, :meth:`edge_attributes`, :meth:`face_attributes` + + """ + if cell not in self._cell: + raise KeyError(cell) + if names and values is not None: + for name, value in zip(names, values): + if cell not in self._cell_data: + self._cell_data[cell] = {} + self._cell_data[cell][name] = value + return + if not names: + return CellAttributeView(self.default_cell_attributes, self._cell_data.setdefault(cell, {})) + values = [] + for name in names: + value = self.cell_attribute(cell, name) + values.append(value) + return values + + def cells_attribute(self, name, value=None, cells=None): + """Get or set an attribute of multiple cells. + + Parameters + ---------- + name : str + The name of the attribute. + value : object, optional + The value of the attribute. + cells : list[int], optional + A list of cell identifiers. + + Returns + ------- + list[Any] | None + A list containing the value per face of the requested attribute, + or None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the cells does not exist. + + See Also + -------- + :meth:`cell_attribute`, :meth:`cell_attributes`, :meth:`cells_attributes` + :meth:`vertex_attribute`, :meth:`edge_attribute`, :meth:`face_attribute` + + """ + if not cells: + cells = self.cells() + if value is not None: + for cell in cells: + self.cell_attribute(cell, name, value) + return + return [self.cell_attribute(cell, name) for cell in cells] + + def cells_attributes(self, names=None, values=None, cells=None): + """Get or set multiple attributes of multiple cells. + + Parameters + ---------- + names : list[str], optional + The names of the attribute. + Default is None. + values : list[Any], optional + The values of the attributes. + Default is None. + cells : list[int], optional + A list of cell identifiers. + + Returns + ------- + list[dict[str, Any]] | list[list[Any]] | None + If the parameter `names` is empty, + a list containing per cell an attribute dict with all attributes (default + custom) of the cell. + If the parameter `names` is empty, + a list containing per cell a list of attribute values corresponding to the requested names. + None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the faces does not exist. + + See Also + -------- + :meth:`cell_attribute`, :meth:`cell_attributes`, :meth:`cells_attribute` + :meth:`vertex_attributes`, :meth:`edge_attributes`, :meth:`face_attributes` + + """ + if not cells: + cells = self.cells() + if values is not None: + for cell in cells: + self.cell_attributes(cell, names, values) + return + return [self.cell_attributes(cell, names) for cell in cells] + + # -------------------------------------------------------------------------- + # Cell Topology + # -------------------------------------------------------------------------- + + def cell_vertices(self, cell): + """The vertices of a cell. + + Parameters + ---------- + cell : int + Identifier of the cell. + + Returns + ------- + list[int] + The vertex identifiers of a cell. + + See Also + -------- + :meth:`cell_edges`, :meth:`cell_faces`, :meth:`cell_halfedges` + + Notes + ----- + This method is similar to :meth:`~compas.datastructures.HalfEdge.vertices`, + but in the context of a cell of the `VolMesh`. + + """ + return list(set([vertex for face in self.cell_faces(cell) for vertex in self.face_vertices(face)])) + + def cell_halfedges(self, cell): + """The halfedges of a cell. + + Parameters + ---------- + cell : int + Identifier of the cell. + + Returns + ------- + list[tuple[int, int]] + The halfedges of a cell. + + See Also + -------- + :meth:`cell_edges`, :meth:`cell_faces`, :meth:`cell_vertices` + + Notes + ----- + This method is similar to :meth:`~compas.datastructures.HalfEdge.halfedges`, + but in the context of a cell of the `VolMesh`. + + """ + halfedges = [] + for u in self._cell[cell]: + for v in self._cell[cell][u]: + halfedges.append((u, v)) + return halfedges + + def cell_edges(self, cell): + """Return all edges of a cell. + + Parameters + ---------- + cell : int + The cell identifier. + + Returns + ------- + list[tuple[int, int]] + The edges of the cell. + + See Also + -------- + :meth:`cell_halfedges`, :meth:`cell_faces`, :meth:`cell_vertices` + + Notes + ----- + This method is similar to :meth:`~compas.datastructures.HalfEdge.edges`, + but in the context of a cell of the `VolMesh`. + + """ + return self.cell_halfedges(cell) + + def cell_faces(self, cell): + """The faces of a cell. + + Parameters + ---------- + cell : int + Identifier of the cell. + + Returns + ------- + list[int] + The faces of a cell. + + See Also + -------- + :meth:`cell_halfedges`, :meth:`cell_edges`, :meth:`cell_vertices` + + Notes + ----- + This method is similar to :meth:`~compas.datastructures.HalfEdge.faces`, + but in the context of a cell of the `VolMesh`. + + """ + faces = set() + for vertex in self._cell[cell]: + faces.update(self._cell[cell][vertex].values()) + return list(faces) + + def cell_vertex_neighbors(self, cell, vertex): + """Ordered vertex neighbors of a vertex of a cell. + + Parameters + ---------- + cell : int + Identifier of the cell. + vertex : int + Identifier of the vertex. + + Returns + ------- + list[int] + The list of neighboring vertices. + + See Also + -------- + :meth:`cell_vertex_faces` + + Notes + ----- + All of the returned vertices are part of the cell. + + This method is similar to :meth:`~compas.datastructures.HalfEdge.vertex_neighbors`, + but in the context of a cell of the `VolMesh`. + + """ + if vertex not in self._cell[cell]: + raise KeyError(vertex) + + nbrs = [] + for nbr in self._vertex[vertex]: + if nbr in self._cell[cell]: + nbrs.append(nbr) + + return nbrs + + # nbr_vertices = self._cell[cell][vertex].keys() + # v = nbr_vertices[0] + # ordered_vkeys = [v] + # for i in range(len(nbr_vertices) - 1): + # face = self._cell[cell][vertex][v] + # v = self.halfface_vertex_ancestor(face, vertex) + # ordered_vkeys.append(v) + # return ordered_vkeys + + def cell_vertex_faces(self, cell, vertex): + """Ordered faces connected to a vertex of a cell. + + Parameters + ---------- + cell : int + Identifier of the cell. + vertex : int + Identifier of the vertex. + + Returns + ------- + list[int] + The ordered list of faces connected to a vertex of a cell. + + See Also + -------- + :meth:`cell_vertex_neighbors` + + Notes + ----- + All of the returned faces should are part of the same cell. + + This method is similar to :meth:`~compas.datastructures.HalfEdge.vertex_faces`, + but in the context of a cell of the `VolMesh`. + + """ + # nbr_vertices = self._cell[cell][vertex].keys() + # u = vertex + # v = nbr_vertices[0] + # ordered_faces = [] + # for i in range(len(nbr_vertices)): + # face = self._cell[cell][u][v] + # v = self.halfface_vertex_ancestor(face, u) + # ordered_faces.append(face) + # return ordered_faces + + if vertex not in self._cell[cell]: + raise KeyError(vertex) + + faces = [] + for nbr in self._cell[cell][vertex]: + faces.append(self._cell[cell][vertex][nbr]) + + return faces + + def cell_face_vertices(self, cell, face): + """The vertices of a face of a cell. + + Parameters + ---------- + cell : int + Identifier of the cell. + face : int + Identifier of the face. + + Returns + ------- + list[int] + The vertices of the face of the cell. + + See Also + -------- + :meth:`cell_face_halfedges` + + Notes + ----- + All of the returned vertices are part of the cell. + + This method is similar to :meth:`~compas.datastructures.HalfEdge.face_vertices`, + but in the context of a cell of the `VolMesh`. + + """ + if face not in self._face: + raise KeyError(face) + + vertices = self.face_vertices(face) + u, v = vertices[:2] + if v in self._cell[cell][u] and self._cell[cell][u][v] == face: + return self.face_vertices(face) + if u in self._cell[cell][v] and self._cell[cell][v][u] == face: + return self.face_vertices(face)[::-1] + + raise Exception("Face is not part of the cell") + + def cell_face_halfedges(self, cell, face): + """The halfedges of a face of a cell. + + Parameters + ---------- + cell : int + Identifier of the cell. + face : int + Identifier of the face. + + Returns + ------- + list[tuple[int, int]] + The halfedges of the face of the cell. + + See Also + -------- + :meth:`cell_face_vertices` + + Notes + ----- + All of the returned halfedges are part of the cell. + + This method is similar to :meth:`~compas.datastructures.HalfEdge.face_halfedges`, + but in the context of a cell of the `VolMesh`. + + """ + vertices = self.cell_face_vertices(cell, face) + return list(pairwise(vertices + vertices[:1])) + + def cell_halfedge_face(self, cell, halfedge): + """Find the face corresponding to a specific halfedge of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + halfedge : tuple[int, int] + The identifier of the halfedge. + + Returns + ------- + int + The identifier of the face. + + See Also + -------- + :meth:`cell_halfedge_opposite_face` + + Notes + ----- + This method is similar to :meth:`~compas.datastructures.HalfEdge.halfedge_face`, + but in the context of a cell of the `VolMesh`. + + """ + u, v = halfedge + if u not in self._cell[cell] or v not in self._cell[cell][u]: + raise KeyError(halfedge) + return self._cell[cell][u][v] + + # def cell_halfedge_opposite_face(self, cell, halfedge): + # """Find the opposite face corresponding to a specific halfedge of a cell. + + # Parameters + # ---------- + # cell : int + # The identifier of the cell. + # halfedge : tuple[int, int] + # The identifier of the halfedge. + + # Returns + # ------- + # int + # The identifier of the face. + + # See Also + # -------- + # :meth:`cell_halfedge_face` + + # """ + # u, v = halfedge + # return self._cell[cell][v][u] + + def cell_face_neighbors(self, cell, face): + """Find the faces adjacent to a given face of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + face : int + The identifier of the face. + + Returns + ------- + int + The identifier of the face. + + See Also + -------- + :meth:`cell_neighbors` + + Notes + ----- + This method is similar to :meth:`~compas.datastructures.HalfEdge.face_neighbors`, + but in the context of a cell of the `VolMesh`. + + """ + # nbrs = [] + # for halfedge in self.halfface_halfedges(face): + # nbr = self.cell_halfedge_opposite_face(cell, halfedge) + # if nbr is not None: + # nbrs.append(nbr) + # return nbrs + + nbrs = [] + for u in self.face_vertices(face): + for v in self._cell[cell][u]: + test = self._cell[cell][u][v] + if test == face: + nbr = self._cell[cell][v][u] + if nbr is not None: + 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. + + # Returns + # ------- + # list[int] + # The identifiers of the adjacent cells. + + # See Also + # -------- + # :meth:`cell_face_neighbors` + + # """ + # nbrs = [] + # for face in self.cell_faces(cell): + # nbr = self.halfface_opposite_cell(face) + # if nbr is not None: + # nbrs.append(nbr) + # return nbrs + + def is_cell_on_boundary(self, cell): + """Verify that a cell is on the boundary. + + Parameters + ---------- + cell : int + Identifier of the cell. + + Returns + ------- + bool + True if the face is on the boundary. + False otherwise. + + See Also + -------- + :meth:`is_vertex_on_boundary`, :meth:`is_edge_on_boundary`, :meth:`is_face_on_boundary` + + """ + faces = self.cell_faces(cell) + for face in faces: + if self.is_face_on_boundary(face): + return True + return False + + def cells_on_boundaries(self): + """Find the cells on the boundary. + + Returns + ------- + list[int] + The cells of the boundary. + + See Also + -------- + :meth:`vertices_on_boundaries`, :meth:`faces_on_boundaries` + + """ + cells = [] + for cell in self.cells(): + if self.is_cell_on_boundary(cell): + cells.append(cell) + return cells + + # -------------------------------------------------------------------------- + # Cell Geometry + # -------------------------------------------------------------------------- + + def cell_points(self, cell): + """Compute the points of the vertices of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + + Returns + ------- + list[:class:`compas.geometry.Point`] + The points of the vertices of the cell. + + See Also + -------- + :meth:`cell_polygon`, :meth:`cell_centroid`, :meth:`cell_center` + """ + return [self.vertex_point(vertex) for vertex in self.cell_vertices(cell)] + + def cell_centroid(self, cell): + """Compute the point at the centroid of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + + Returns + ------- + :class:`compas.geometry.Point` + The coordinates of the centroid. + + See Also + -------- + :meth:`cell_center` + """ + vertices = self.cell_vertices(cell) + return Point(*centroid_points([self.vertex_coordinates(vertex) for vertex in vertices])) + + def cell_center(self, cell): + """Compute the point at the center of mass of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + + Returns + ------- + :class:`compas.geometry.Point` + The coordinates of the center of mass. + + See Also + -------- + :meth:`cell_centroid` + """ + vertices, faces = self.cell_to_vertices_and_faces(cell) + return Point(*centroid_polyhedron((vertices, faces))) + + # def cell_vertex_normal(self, cell, vertex): + # """Return the normal vector at the vertex of a boundary cell as the weighted average of the + # normals of the neighboring faces. + + # Parameters + # ---------- + # cell : int + # The identifier of the vertex of the cell. + # vertex : int + # The identifier of the vertex of the cell. + + # Returns + # ------- + # :class:`compas.geometry.Vector` + # The components of the normal vector. + + # """ + # cell_faces = self.cell_faces(cell) + # vectors = [self.face_normal(face) for face in self.vertex_halffaces(vertex) if face in cell_faces] + # return Vector(*normalize_vector(centroid_points(vectors))) + + def cell_polyhedron(self, cell): + """Construct a polyhedron from the vertices and faces of a cell. + + Parameters + ---------- + cell : int + The identifier of the cell. + + Returns + ------- + :class:`compas.geometry.Polyhedron` + The polyhedron. + + """ + vertices, faces = self.cell_to_vertices_and_faces(cell) + return Polyhedron(vertices, faces) + + # # -------------------------------------------------------------------------- + # # Boundaries + # # -------------------------------------------------------------------------- + + # def vertices_on_boundaries(self): + # """Find the vertices on the boundary. + + # Returns + # ------- + # list[int] + # The vertices of the boundary. + + # See Also + # -------- + # :meth:`faces_on_boundaries`, :meth:`cells_on_boundaries` + + # """ + # vertices = set() + # for face in self._halfface: + # if self.is_halfface_on_boundary(face): + # vertices.update(self.halfface_vertices(face)) + # return list(vertices) + + # def halffaces_on_boundaries(self): + # """Find the faces on the boundary. + + # Returns + # ------- + # list[int] + # The faces of the boundary. + + # See Also + # -------- + # :meth:`vertices_on_boundaries`, :meth:`cells_on_boundaries` + + # """ + # faces = set() + # for face in self._halfface: + # if self.is_halfface_on_boundary(face): + # faces.add(face) + # return list(faces) + + # def cells_on_boundaries(self): + # """Find the cells on the boundary. + + # Returns + # ------- + # list[int] + # The cells of the boundary. + + # See Also + # -------- + # :meth:`vertices_on_boundaries`, :meth:`faces_on_boundaries` + + # """ + # cells = set() + # for face in self.halffaces_on_boundaries(): + # cells.add(self.halfface_cell(face)) + # return list(cells) + + # # -------------------------------------------------------------------------- + # # Transformations + # # -------------------------------------------------------------------------- + + # def transform(self, T): + # """Transform the mesh. + + # Parameters + # ---------- + # T : :class:`Transformation` + # The transformation used to transform the mesh. + + # Returns + # ------- + # None + # The mesh is modified in-place. + + # Examples + # -------- + # >>> from compas.datastructures import Mesh + # >>> from compas.geometry import matrix_from_axis_and_angle + # >>> mesh = Mesh.from_polyhedron(6) + # >>> T = matrix_from_axis_and_angle([0, 0, 1], math.pi / 4) + # >>> mesh.transform(T) + + # """ + # points = transform_points(self.vertices_attributes("xyz"), T) + # for vertex, point in zip(self.vertices(), points): + # self.vertex_attributes(vertex, "xyz", point) diff --git a/src/compas/scene/cellnetworkobject.py b/src/compas/scene/cellnetworkobject.py new file mode 100644 index 00000000000..8739516fe56 --- /dev/null +++ b/src/compas/scene/cellnetworkobject.py @@ -0,0 +1,252 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from compas.geometry import transform_points + +from .descriptors.colordict import ColorDictAttribute +from .sceneobject import SceneObject + + +class CellNetworkObject(SceneObject): + """Scene object for drawing volmesh data structures. + + Parameters + ---------- + volmesh : :class:`compas.datastructures.VolMesh` + A COMPAS volmesh. + + Attributes + ---------- + volmesh : :class:`compas.datastructures.VolMesh` + The COMPAS volmesh associated with the scene object. + vertex_xyz : dict[int, list[float]] + The view coordinates of the vertices. + By default, the actual vertex coordinates are used. + vertexcolor : :class:`compas.colors.ColorDict` + Mapping between vertices and colors. + Missing vertices get the default vertex color: :attr:`default_vertexcolor`. + edgecolor : :class:`compas.colors.ColorDict` + Mapping between edges and colors. + Missing edges get the default edge color: :attr:`default_edgecolor`. + facecolor : :class:`compas.colors.ColorDict` + Mapping between faces and colors. + Missing faces get the default face color: :attr:`default_facecolor`. + cellcolor : :class:`compas.colors.ColorDict` + Mapping between cells and colors. + Missing cells get the default cell color: :attr:`default_facecolor`. + vertexsize : float + 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 ``False``. + show_cells : bool + Flag for showing or hiding the cells, or a list of keys for the cells to show. + Default is ``True``. + + See Also + -------- + :class:`compas.scene.GraphObject` + :class:`compas.scene.MeshObject` + + """ + + vertexcolor = ColorDictAttribute() + edgecolor = ColorDictAttribute() + facecolor = ColorDictAttribute() + cellcolor = ColorDictAttribute() + + def __init__(self, volmesh, **kwargs): + super(CellNetworkObject, self).__init__(item=volmesh, **kwargs) + self._volmesh = None + 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) + + @property + def volmesh(self): + return self._volmesh + + @volmesh.setter + def volmesh(self, volmesh): + self._volmesh = volmesh + self._transformation = None + self._vertex_xyz = None + + @property + def transformation(self): + return self._transformation + + @transformation.setter + def transformation(self, transformation): + self._vertex_xyz = None + self._transformation = transformation + + @property + def vertex_xyz(self): + 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 + + @vertex_xyz.setter + def vertex_xyz(self, vertex_xyz): + self._vertex_xyz = vertex_xyz + + def draw_vertices(self, vertices=None, color=None, text=None): + """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. + + Returns + ------- + list + The identifiers of the objects representing the vertices in the visualization context. + + """ + raise NotImplementedError + + def draw_edges(self, edges=None, color=None, text=None): + """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. + + Returns + ------- + list + The identifiers of the objects representing the edges in the visualization context. + + """ + raise NotImplementedError + + def draw_faces(self, faces=None, color=None, text=None): + """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. + + Returns + ------- + list + The identifiers of the objects representing the faces in the visualization context. + + """ + raise NotImplementedError + + def draw_cells(self, cells=None, color=None, text=None): + """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. + + Returns + ------- + list + The identifiers of the objects representing the cells in the visualization context. + + """ + raise NotImplementedError + + def draw(self): + """Draw the volmesh.""" + raise NotImplementedError + + def clear_vertices(self): + """Clear the vertices of the mesh. + + Returns + ------- + None + + """ + raise NotImplementedError + + def clear_edges(self): + """Clear the edges of the mesh. + + Returns + ------- + None + + """ + raise NotImplementedError + + def clear_faces(self): + """Clear the faces of the mesh. + + Returns + ------- + None + + """ + raise NotImplementedError + + def clear_cells(self): + """Clear the cells of the mesh. + + Returns + ------- + None + + """ + raise NotImplementedError From 9933e738715521c02a153cd88bd614499d103f1b Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Sun, 26 May 2024 09:27:29 +0200 Subject: [PATCH 09/21] check for number of elements in stack --- src/compas/geometry/icp_numpy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/compas/geometry/icp_numpy.py b/src/compas/geometry/icp_numpy.py index ccd905ad351..661e656b8d6 100644 --- a/src/compas/geometry/icp_numpy.py +++ b/src/compas/geometry/icp_numpy.py @@ -93,7 +93,7 @@ def icp_numpy(source, target, tol=None, maxiter=100): X = Transformation.from_frame_to_frame(A_frame, B_frame) A = transform_points_numpy(A, X) - stack = [X] + stack = [asarray(X.matrix)] for i in range(maxiter): D = cdist(A, B, "euclidean") @@ -108,4 +108,6 @@ def icp_numpy(source, target, tol=None, maxiter=100): stack.append(X) + if len(stack) == 1: + return stack[0] return A, multi_dot(stack[::-1]) From 395f863a77fbfe3c22230e3605805f51de6241d5 Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Sun, 26 May 2024 16:26:50 +0200 Subject: [PATCH 10/21] updates --- ...astructures.cellnetworks.example_color.png | Bin 0 -> 63980 bytes ...tastructures.cellnetworks.example_grey.png | Bin 0 -> 43037 bytes .../basics.datastructures.cell_networks.py | 102 --- .../basics.datastructures.cell_networks.rst | 30 - .../basics.datastructures.cellnetwork.rst | 15 +- .../basics.datastructures.cellnetworks.py | 40 + docs/userguide/network.json | 1 - docs/userguide/test.ipynb | 299 ------- docs/userguide/test.py | 62 -- .../data/samples/cellnetwork_example.json | 744 ++++++++++++++++++ .../cell_network/cell_network copy.py | 41 +- .../cell_network/cell_network.py | 182 +++-- 12 files changed, 943 insertions(+), 573 deletions(-) create mode 100644 docs/_images/userguide/basics.datastructures.cellnetworks.example_color.png create mode 100644 docs/_images/userguide/basics.datastructures.cellnetworks.example_grey.png delete mode 100644 docs/userguide/basics.datastructures.cell_networks.py delete mode 100644 docs/userguide/basics.datastructures.cell_networks.rst create mode 100644 docs/userguide/basics.datastructures.cellnetworks.py delete mode 100644 docs/userguide/network.json delete mode 100644 docs/userguide/test.ipynb delete mode 100644 docs/userguide/test.py create mode 100644 src/compas/data/samples/cellnetwork_example.json 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 0000000000000000000000000000000000000000..4d2d08869737578495035b950264c2a6427890c9 GIT binary patch literal 63980 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

;iCy$A#`Q80rV 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8f2a740579f0a1d14344880d70f659a163cedbed GIT binary patch literal 43037 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 literal 0 HcmV?d00001 diff --git a/docs/userguide/basics.datastructures.cell_networks.py b/docs/userguide/basics.datastructures.cell_networks.py deleted file mode 100644 index 1d4db205e32..00000000000 --- a/docs/userguide/basics.datastructures.cell_networks.py +++ /dev/null @@ -1,102 +0,0 @@ -from compas.geometry import Point, Vector, Frame, Box, close - -from scipy.spatial import cKDTree - -boxes = [Box(4.5, 9.5, 3.0, Frame(Point(2.250, 4.750, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), -Box(3.0, 5.0, 3.0, Frame(Point(1.500, 2.500, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), -Box(8.0, 4.5, 3.0, Frame(Point(4.000, 2.250, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), -Box(5.0, 5.0, 3.0, Frame(Point(2.500, 2.500, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), -Box(7.5, 6.5, 3.0, Frame(Point(3.750, 3.250, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))), -Box(5.0, 6.5, 3.0, Frame(Point(2.500, 3.250, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000)))] - -tt_faces = [[Point(5.000, 8.000, 6.000), Point(0.000, 8.000, 6.000), Point(5.000, 3.000, 6.000), Point(0.000, 3.000, 6.000)], -[Point(14.500, -1.500, 6.000), Point(14.500, -1.500, 3.000), Point(12.500, -1.500, 6.000), Point(12.500, -1.500, 3.000)], -[Point(12.500, -1.500, 6.000), Point(12.500, 8.000, 6.000), Point(14.500, 8.000, 6.000), Point(14.500, -1.500, 6.000), Point(12.500, -1.500, 6.000)], -[Point(12.500, -1.500, 3.000), Point(12.500, 8.000, 3.000), Point(14.500, 8.000, 3.000), Point(14.500, -1.500, 3.000), Point(12.500, -1.500, 3.000)], -[Point(14.500, 8.000, 4.200), Point(14.500, 8.000, 3.000), Point(14.500, -1.500, 4.200), Point(14.500, -1.500, 3.000)], -[Point(12.500, 8.000, 6.000), Point(12.500, 8.000, 3.000), Point(14.500, 8.000, 6.000), Point(14.500, 8.000, 3.000)], -] - -points = [] -for box in boxes: - for pt in box.to_vertices_and_faces()[0]: - if not len(points): - points.append(pt) - continue - - tree = cKDTree(points) - d, idx = tree.query(pt, k=1) - if close(d, 0, 1e-3): - pass - else: - points.append(pt) - -for face in tt_faces: - for pt in face: - tree = cKDTree(points) - d, idx = tree.query(pt, k=1) - if close(d, 0, 1e-3): - pass - else: - points.append(pt) - -print(len(points)) -tree = cKDTree(points) - -from compas.datastructures import CellNetwork - -network = CellNetwork() - -for i, (x, y, z) in enumerate(points): - network.add_vertex(key=i, x=x, y=y, z=z) - -for box in boxes: - verts, faces = box.to_vertices_and_faces() - fidxs = [] - for face in faces: - nface = [] - for idx in face: - pt = verts[idx] - d, ni = tree.query(pt, k=1) - nface.append(ni) - fidx = None - for face in network.faces(): - if set( network.face_vertices(face)) == set(nface): - fidx = face - break - else: - fidx = network.add_face(nface) - fidxs.append(fidx) - network.add_cell(fidxs) - print(fidxs) - - -for face in tt_faces: - - nface = [] - for pt in face: - d, ni = tree.query(pt, k=1) - nface.append(ni) - - fidx = None - for face in network.faces(): - if set( network.face_vertices(face)) == set(nface): - fidx = face - break - else: - fidx = network.add_face(nface) - - -from compas_viewer import Viewer - -viewer = Viewer() - -viewer.add(network, show_faces=True, show_edges=True, show_vertices=True) -viewer.show() - -""" - [network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] - [network.add_edge(u, v) for u, v in edges] - [network.add_face(fverts) for fverts in faces] - [network.add_cell(fkeys) for fkeys in cells] -""" diff --git a/docs/userguide/basics.datastructures.cell_networks.rst b/docs/userguide/basics.datastructures.cell_networks.rst deleted file mode 100644 index 4bcb5a62391..00000000000 --- a/docs/userguide/basics.datastructures.cell_networks.rst +++ /dev/null @@ -1,30 +0,0 @@ -******************************************************************************** -Cell Networks -******************************************************************************** - -.. rst-class:: lead - -A `compas.datastructures.CellNetwork` is a geometric implementation of a data structure for storing a -collection of mixed topologic entities such as cells, faces, edges and vertices. -It can be used to describe buildings; walls and floors can be represented as faces, columns, beams as edges, and rooms as cells. -Aperatures such as windows or doors could be stored as face attributes. -Topological queries such as "what is the building envelope" can be easily be derived. - -.. note:: - - Please refer to the API for a complete overview of all functionality: - - * :class:`compas.datastructures.HalfFace` - * :class:`compas.datastructures.CellNetwork` - - -CellNetwork Construction -======================== - -Cell networks 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. - - diff --git a/docs/userguide/basics.datastructures.cellnetwork.rst b/docs/userguide/basics.datastructures.cellnetwork.rst index a8e7358e1c1..55bd266a27c 100644 --- a/docs/userguide/basics.datastructures.cellnetwork.rst +++ b/docs/userguide/basics.datastructures.cellnetwork.rst @@ -13,10 +13,11 @@ In addition, it provides a number of methods for storing arbitrary data on verti Please refer to the API for a complete overview of all functionality: * :class:`compas.datastructures.CellNetwork` + * :class:`compas.datastructures.HalfFace` CellNetwork Construction -================= +======================== Meshes can be constructed in a number of ways: @@ -61,14 +62,18 @@ For more information about visualisation with :class:`compas.scene.Scene`, see : >>> from compas.datastructures import CellNetwork >>> from compas.scene import Scene ->>> cell_network = CellNetwork.from_json(compas.get('tubemesh.obj')) +>>> 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 +The cell network contains mixed topologic entities such as cells, faces, edges and vertices. +A face can be at maximum assigned to two cells, to one or None. +Faces have typically edges as boundaries, but an edge can also exist without being part of a face. +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 belonging to no 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..45cf3b12e25 --- /dev/null +++ b/docs/userguide/basics.datastructures.cellnetworks.py @@ -0,0 +1,40 @@ +import os + +from compas_viewer import Viewer + +import compas +from compas.colors import Color +from compas.geometry import Line, Polygon + +HERE = os.path.dirname(__file__) +cell_network = compas.json_load(os.path.join(HERE, "basics.datastructures.cell_networks.json")) + +""" +viewer = Viewer() +for face in cell_network.faces(): + viewer.scene.add(Polygon(cell_network.face_coordinates(face)), facecolor=Color.silver()) +for edge in cell_network.edges_without_face(): + line = Line(*cell_network.edge_coordinates(edge)) + viewer.scene.add(line, linewidth=3) +viewer.show() +""" + + +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 = Color.silver() + opacity = 0.5 + elif face in no_cell: + color = Color.azure() + opacity = 0.3 + else: + color = Color.yellow() + opacity = 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) +viewer.show() + diff --git a/docs/userguide/network.json b/docs/userguide/network.json deleted file mode 100644 index 4b2807af9b7..00000000000 --- a/docs/userguide/network.json +++ /dev/null @@ -1 +0,0 @@ -{"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": 0.0, "y": 0.0, "z": 0.0}, "1": {"x": 0.0, "y": 9.5, "z": 0.0}, "2": {"x": 4.5, "y": 9.5, "z": 0.0}, "3": {"x": 4.5, "y": 0.0, "z": 0.0}, "4": {"x": 0.0, "y": 0.0, "z": 3.0}, "5": {"x": 4.5, "y": 0.0, "z": 3.0}, "6": {"x": 4.5, "y": 9.5, "z": 3.0}, "7": {"x": 0.0, "y": 9.5, "z": 3.0}, "8": {"x": 5.0, "y": 8.0, "z": 6.0}, "9": {"x": 0.0, "y": 8.0, "z": 6.0}, "10": {"x": 5.0, "y": 3.0, "z": 6.0}, "11": {"x": 0.0, "y": 3.0, "z": 6.0}, "12": {"x": 14.5, "y": -1.5, "z": 6.0}, "13": {"x": 14.5, "y": -1.5, "z": 3.0}, "14": {"x": 12.5, "y": -1.5, "z": 6.0}, "15": {"x": 12.5, "y": -1.5, "z": 3.0}, "16": {"x": 12.5, "y": 8.0, "z": 6.0}, "17": {"x": 14.5, "y": 8.0, "z": 6.0}, "18": {"x": 12.5, "y": 8.0, "z": 3.0}, "19": {"x": 14.5, "y": 8.0, "z": 3.0}, "20": {"x": 14.5, "y": 8.0, "z": 4.2}, "21": {"x": 14.5, "y": -1.5, "z": 4.2}}, "edge": {"0": {"3": {}, "4": {}}, "1": {"0": {}}, "2": {"1": {}}, "3": {"2": {}}, "4": {"5": {}}, "5": {"3": {}, "6": {}}, "6": {"2": {}, "7": {}}, "7": {"1": {}, "4": {}}, "8": {"11": {}}, "9": {"8": {}}, "10": {"9": {}}, "11": {"10": {}}, "12": {"15": {}, "17": {}}, "13": {"12": {}, "19": {}, "21": {}}, "14": {"13": {}, "12": {}}, "15": {"14": {}, "13": {}}, "16": {"14": {}, "19": {}}, "17": {"16": {}, "18": {}}, "18": {"15": {}, "16": {}}, "19": {"18": {}, "20": {}, "17": {}}, "20": {"13": {}}, "21": {"19": {}}}, "face": {"0": [0, 1, 2, 3], "1": [0, 3, 5, 4], "2": [3, 2, 6, 5], "3": [2, 1, 7, 6], "4": [1, 0, 4, 7], "5": [4, 5, 6, 7], "6": [8, 9, 10, 11], "7": [12, 13, 14, 15], "8": [14, 16, 17, 12], "9": [15, 18, 19, 13], "10": [20, 19, 21, 13], "11": [16, 18, 17, 19]}, "cell": {"0": [0, 1, 2, 3, 4, 5]}, "face_data": {}, "cell_data": {}, "max_vertex": 21, "max_face": 11, "max_cell": 0}, "guid": "dc7bbd81-65a7-438c-a653-66ee653cf1d1"} \ No newline at end of file diff --git a/docs/userguide/test.ipynb b/docs/userguide/test.ipynb deleted file mode 100644 index ec6cfdf3fc1..00000000000 --- a/docs/userguide/test.ipynb +++ /dev/null @@ -1,299 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22\n", - "[0, 1, 2, 3, 4, 5]\n" - ] - } - ], - "source": [ - "from compas.geometry import Point, Vector, Frame, Box, close\n", - "\n", - "from scipy.spatial import cKDTree\n", - "\n", - "boxes = [Box(4.5, 9.5, 3.0, Frame(Point(2.250, 4.750, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", - "#Box(3.0, 5.0, 3.0, Frame(Point(1.500, 2.500, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", - "]\n", - "#Box(8.0, 4.5, 3.0, Frame(Point(4.000, 2.250, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", - "#Box(5.0, 5.0, 3.0, Frame(Point(2.500, 2.500, 1.500), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", - "#Box(7.5, 6.5, 3.0, Frame(Point(3.750, 3.250, 4.50), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000))),\n", - "#Box(5.0, 6.5, 3.0, Frame(Point(2.500, 3.250, 4.50), Vector(1.000, 0.000, 0.000), Vector(0.000, 1.000, 0.000)))]\n", - "\n", - "tt_faces = [[Point(5.000, 8.000, 6.000), Point(0.000, 8.000, 6.000), Point(5.000, 3.000, 6.000), Point(0.000, 3.000, 6.000)],\n", - "[Point(14.500, -1.500, 6.000), Point(14.500, -1.500, 3.000), Point(12.500, -1.500, 6.000), Point(12.500, -1.500, 3.000)],\n", - "[Point(12.500, -1.500, 6.000), Point(12.500, 8.000, 6.000), Point(14.500, 8.000, 6.000), Point(14.500, -1.500, 6.000), Point(12.500, -1.500, 6.000)],\n", - "[Point(12.500, -1.500, 3.000), Point(12.500, 8.000, 3.000), Point(14.500, 8.000, 3.000), Point(14.500, -1.500, 3.000), Point(12.500, -1.500, 3.000)],\n", - "[Point(14.500, 8.000, 4.200), Point(14.500, 8.000, 3.000), Point(14.500, -1.500, 4.200), Point(14.500, -1.500, 3.000)],\n", - "[Point(12.500, 8.000, 6.000), Point(12.500, 8.000, 3.000), Point(14.500, 8.000, 6.000), Point(14.500, 8.000, 3.000)],\n", - "]\n", - "\n", - "points = []\n", - "for box in boxes:\n", - " for pt in box.to_vertices_and_faces()[0]:\n", - " if not len(points):\n", - " points.append(pt)\n", - " continue\n", - "\n", - " tree = cKDTree(points)\n", - " d, idx = tree.query(pt, k=1)\n", - " if close(d, 0, 1e-3):\n", - " pass\n", - " else:\n", - " points.append(pt)\n", - "\n", - "for face in tt_faces:\n", - " for pt in face:\n", - " tree = cKDTree(points)\n", - " d, idx = tree.query(pt, k=1)\n", - " if close(d, 0, 1e-3):\n", - " pass\n", - " else:\n", - " points.append(pt)\n", - "\n", - "print(len(points))\n", - "tree = cKDTree(points)\n", - "\n", - "from compas.datastructures import CellNetwork\n", - "\n", - "network = CellNetwork()\n", - "\n", - "for i, (x, y, z) in enumerate(points):\n", - " network.add_vertex(key=i, x=x, y=y, z=z)\n", - "\n", - "for box in boxes:\n", - " verts, faces = box.to_vertices_and_faces()\n", - " fidxs = []\n", - " for face in faces:\n", - " nface = []\n", - " for idx in face:\n", - " pt = verts[idx]\n", - " d, ni = tree.query(pt, k=1)\n", - " nface.append(ni)\n", - " fidx = None\n", - " for face in network.faces():\n", - " if set( network.face_vertices(face)) == set(nface):\n", - " fidx = face\n", - " break\n", - " else:\n", - " fidx = network.add_face(nface)\n", - " fidxs.append(fidx)\n", - " network.add_cell(fidxs)\n", - " print(fidxs)\n", - "\n", - "\n", - "for face in tt_faces:\n", - "\n", - " nface = []\n", - " for pt in face:\n", - " d, ni = tree.query(pt, k=1)\n", - " nface.append(ni)\n", - "\n", - " fidx = None\n", - " for face in network.faces():\n", - " if set( network.face_vertices(face)) == set(nface):\n", - " fidx = face\n", - " break\n", - " else:\n", - " fidx = network.add_face(nface)\n", - "\n", - "\n", - "\n", - "\n", - "\"\"\"\n", - " [network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices]\n", - " [network.add_edge(u, v) for u, v in edges]\n", - " [network.add_face(fverts) for fverts in faces]\n", - " [network.add_cell(fkeys) for fkeys in cells]\n", - "\"\"\"\n", - "network.to_json('/Users/romanarust/workspace/compas_dev/compas/docs/userguide/network.json')\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[0, 1, 2, 3], [0, 3, 5, 4], [3, 2, 6, 5], [2, 1, 7, 6], [1, 0, 4, 7], [4, 5, 6, 7]]\n", - "\n" - ] - } - ], - "source": [ - "from compas.geometry import Box\n", - "from compas.datastructures import CellNetwork\n", - "\n", - "network = CellNetwork()\n", - "\n", - "vertices, faces = Box(1).to_vertices_and_faces()\n", - "for x, y, z in vertices:\n", - " network.add_vertex(x=x, y=y, z=z)\n", - "print(faces)\n", - "fkeys = []\n", - "for face in faces:\n", - " fkeys.append(network.add_face(face))\n", - "network.add_cell(fkeys)\n", - "\n", - "\n", - "print(network)\n", - "\n", - " \n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "8\n" - ] - }, - { - "ename": "KeyError", - "evalue": "0", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[7], line 9\u001b[0m\n\u001b[1;32m 7\u001b[0m cells \u001b[38;5;241m=\u001b[39m [[\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m3\u001b[39m, \u001b[38;5;241m4\u001b[39m, \u001b[38;5;241m5\u001b[39m]]\n\u001b[1;32m 8\u001b[0m [network\u001b[38;5;241m.\u001b[39madd_vertex(x\u001b[38;5;241m=\u001b[39mx, y\u001b[38;5;241m=\u001b[39my, z\u001b[38;5;241m=\u001b[39mz) \u001b[38;5;28;01mfor\u001b[39;00m x, y, z \u001b[38;5;129;01min\u001b[39;00m vertices]\n\u001b[0;32m----> 9\u001b[0m [cell_network\u001b[38;5;241m.\u001b[39madd_face(fverts) \u001b[38;5;28;01mfor\u001b[39;00m fverts \u001b[38;5;129;01min\u001b[39;00m faces]\n\u001b[1;32m 10\u001b[0m [cell_network\u001b[38;5;241m.\u001b[39madd_cell(fkeys) \u001b[38;5;28;01mfor\u001b[39;00m fkeys \u001b[38;5;129;01min\u001b[39;00m cells]\n\u001b[1;32m 11\u001b[0m cell_network\n", - "Cell \u001b[0;32mIn[7], line 9\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 7\u001b[0m cells \u001b[38;5;241m=\u001b[39m [[\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m3\u001b[39m, \u001b[38;5;241m4\u001b[39m, \u001b[38;5;241m5\u001b[39m]]\n\u001b[1;32m 8\u001b[0m [network\u001b[38;5;241m.\u001b[39madd_vertex(x\u001b[38;5;241m=\u001b[39mx, y\u001b[38;5;241m=\u001b[39my, z\u001b[38;5;241m=\u001b[39mz) \u001b[38;5;28;01mfor\u001b[39;00m x, y, z \u001b[38;5;129;01min\u001b[39;00m vertices]\n\u001b[0;32m----> 9\u001b[0m [\u001b[43mcell_network\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43madd_face\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfverts\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m fverts \u001b[38;5;129;01min\u001b[39;00m faces]\n\u001b[1;32m 10\u001b[0m [cell_network\u001b[38;5;241m.\u001b[39madd_cell(fkeys) \u001b[38;5;28;01mfor\u001b[39;00m fkeys \u001b[38;5;129;01min\u001b[39;00m cells]\n\u001b[1;32m 11\u001b[0m cell_network\n", - "File \u001b[0;32m~/workspace/compas_dev/compas/src/compas/datastructures/cell_network/cell_network.py:714\u001b[0m, in \u001b[0;36mCellNetwork.add_face\u001b[0;34m(self, vertices, fkey, attr_dict, **kwattr)\u001b[0m\n\u001b[1;32m 711\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mface_attribute(fkey, name, value)\n\u001b[1;32m 713\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m u, v \u001b[38;5;129;01min\u001b[39;00m pairwise(vertices \u001b[38;5;241m+\u001b[39m vertices[:\u001b[38;5;241m1\u001b[39m]):\n\u001b[0;32m--> 714\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m v \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_plane\u001b[49m\u001b[43m[\u001b[49m\u001b[43mu\u001b[49m\u001b[43m]\u001b[49m:\n\u001b[1;32m 715\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_plane[u][v] \u001b[38;5;241m=\u001b[39m {}\n\u001b[1;32m 716\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_plane[u][v][fkey] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", - "\u001b[0;31mKeyError\u001b[0m: 0" - ] - } - ], - "source": [ - "from compas.datastructures import CellNetwork\n", - "cell_network = CellNetwork()\n", - "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)]\n", - "\n", - "print(len(vertices))\n", - "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]]\n", - "cells = [[0, 1, 2, 3, 4, 5]]\n", - "[network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices]\n", - "[cell_network.add_face(fverts) for fverts in faces]\n", - "[cell_network.add_cell(fkeys) for fkeys in cells]\n", - "cell_network" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "PyThreeJS SceneObjects registered.\n", - "No triangles found\n", - "No triangles found\n", - "No triangles found\n", - "No triangles found\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4d588dd468c842ebb17463175eb8cfcb", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(HBox(children=(Button(icon='folder-open', layout=Layout(height='32px', width='48px'), style=But…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from compas_notebook.viewer import Viewer\n", - "viewer = Viewer()\n", - "#viewer.scene.add(mesh, color='#cccccc')\n", - "\n", - "cell_network = network\n", - "from compas.geometry import Polygon, Polyhedron\n", - "\n", - "opacity = 0.5\n", - "\n", - "\n", - "\"\"\" \n", - "for face in cell_network.faces_on_boundaries():\n", - " vertices = cell_network.face_coordinates(face)\n", - " viewer.scene.add(Polygon(vertices), color='#cccccc')\n", - "\n", - "\n", - "for face in cell_network.faces_without_cell():\n", - " vertices = cell_network.face_coordinates(face)\n", - " viewer.scene.add(Polygon(vertices), color=\"#7d7eff\")\n", - "\"\"\"\n", - "\n", - "for face in cell_network.faces():\n", - "\n", - " vertices = cell_network.face_coordinates(face)\n", - " try:\n", - " viewer.scene.add(Polygon(vertices), color=\"#7d7eff\")\n", - " except:\n", - " print(face, vertices)\n", - "\n", - "\n", - "\"\"\"\n", - "for face in cell_network.faces():\n", - " cells = cell_network.face_cells(face)\n", - " vertices = cell_network.face_coordinates(face)\n", - "\n", - " if not len(cells):\n", - " pass\n", - " # viewer.add(Polygon(vertices), facecolor=[0.5, 0.55, 1.0], opacity=opacity)\n", - "\n", - " elif len(cells) == 2:\n", - " pass\n", - " # viewer.add(Polygon(vertices), facecolor=[1, 0.8, 0.05], opacity=opacity)\n", - " # break\n", - "\"\"\"\n", - "\n", - "\n", - "\n", - "\n", - "viewer.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "compas-dev", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/userguide/test.py b/docs/userguide/test.py deleted file mode 100644 index aaf0ab2528e..00000000000 --- a/docs/userguide/test.py +++ /dev/null @@ -1,62 +0,0 @@ -from compas.datastructures import CellNetwork -file = "/Users/romanarust/workspace/compas_dev/compas/docs/userguide/network.json" - -network = CellNetwork.from_json(file) - -from compas.geometry import Polygon - -#import compas_view2 - -#print(compas_view2.__file__) -#from compas_view2.app import App - -def show(cell_network): - - - viewer = App() - - opacity = 0.5 - - for face in cell_network.faces_on_boundaries(): - vertices = cell_network.face_coordinates(face) - viewer.add(Polygon(vertices), facecolor=[0.75, 0.75, 0.75], opacity=opacity) - - for face in cell_network.faces_no_cell(): - vertices = cell_network.face_coordinates(face) - viewer.add(Polygon(vertices), facecolor=[0.5, 0.55, 1.0], opacity=opacity) - - for face in cell_network.faces(): - cells = cell_network.face_cells(face) - vertices = cell_network.face_coordinates(face) - - if not len(cells): - pass - # viewer.add(Polygon(vertices), facecolor=[0.5, 0.55, 1.0], opacity=opacity) - - elif len(cells) == 2: - pass - # viewer.add(Polygon(vertices), facecolor=[1, 0.8, 0.05], opacity=opacity) - # break - - viewer.add(cell_network.to_network(), show_lines=True) - viewer.view.camera.zoom_extents() - viewer.show() - -#show(network) - - -from compas.scene import Scene - - - -scene = Scene() -scene.clear() -#scene.add(network) -opacity = 0.5 -cell_network = network - -for face in cell_network.faces_on_boundaries(): - vertices = cell_network.face_coordinates(face) - scene.add(Polygon(vertices), facecolor=[0.75, 0.75, 0.75], opacity=opacity) - -scene.draw() 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 copy.py b/src/compas/datastructures/cell_network/cell_network copy.py index f71bf6174e1..67bc4e7a7a9 100644 --- a/src/compas/datastructures/cell_network/cell_network copy.py +++ b/src/compas/datastructures/cell_network/cell_network copy.py @@ -4,21 +4,22 @@ from random import sample -from compas.datastructures import Mesh from compas.datastructures import Graph -from compas.datastructures.datastructure import Datastructure -from compas.datastructures.attributes import VertexAttributeView +from compas.datastructures import Mesh +from compas.datastructures.attributes import CellAttributeView from compas.datastructures.attributes import EdgeAttributeView from compas.datastructures.attributes import FaceAttributeView -from compas.datastructures.attributes import CellAttributeView - +from compas.datastructures.attributes import VertexAttributeView +from compas.datastructures.datastructure import Datastructure from compas.files import OBJ - from compas.geometry import Line from compas.geometry import Point from compas.geometry import Polygon from compas.geometry import Polyhedron from compas.geometry import Vector +from compas.geometry import add_vectors +from compas.geometry import bestfit_plane +from compas.geometry import bounding_box from compas.geometry import centroid_points from compas.geometry import centroid_polygon from compas.geometry import centroid_polyhedron @@ -26,17 +27,12 @@ from compas.geometry import length_vector from compas.geometry import normal_polygon from compas.geometry import normalize_vector -from compas.geometry import volume_polyhedron -from compas.geometry import add_vectors -from compas.geometry import bestfit_plane from compas.geometry import project_point_plane from compas.geometry import scale_vector from compas.geometry import subtract_vectors -from compas.geometry import bounding_box - -from compas.utilities import pairwise - +from compas.geometry import volume_polyhedron from compas.tolerance import TOL +from compas.utilities import pairwise class CellNetwork(Datastructure): @@ -73,7 +69,7 @@ class CellNetwork(Datastructure): >>> 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]] + >>> 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]] >>> [network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] >>> [cell_network.add_face(fverts) for fverts in faces] @@ -227,15 +223,7 @@ def __from_data__(cls, data): return cell_network - def __init__( - self, - default_vertex_attributes=None, - default_edge_attributes=None, - default_face_attributes=None, - default_cell_attributes=None, - name=None, - **kwargs - ): + def __init__(self, default_vertex_attributes=None, default_edge_attributes=None, default_face_attributes=None, default_cell_attributes=None, name=None, **kwargs): super(CellNetwork, self).__init__(kwargs, name=name) self._max_vertex = -1 self._max_face = -1 @@ -844,7 +832,7 @@ def delete_face(self, face): del self._plane[v][u][face] del self._face[face] if face in self._face_data: - del self._face_data[key] + del self._face_data[face] def delete_cell(self, cell): # remove the cell from the faces @@ -860,11 +848,6 @@ def delete_cell(self, cell): if cell in self._cell_data: del self._cell_data[cell] - - - - - # def delete_cell(self, cell): # """Delete a cell from the cell network. diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index a7c4ce28c49..bc7f8358d12 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -13,6 +14,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 @@ -69,7 +71,7 @@ class CellNetwork(Datastructure): >>> 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]] + >>> 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]] >>> [network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] >>> [cell_network.add_face(fverts) for fverts in faces] @@ -180,6 +182,7 @@ def __data__(self): "edge": self._edge, "face": self._face, "cell": cell, + "edge_data": self._edge_data, "face_data": self._face_data, "cell_data": self._cell_data, "max_vertex": self._max_vertex, @@ -203,19 +206,23 @@ 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 = data.get("edge_data", {}) + print(edge_data) 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]: + print(">>", u, v) + attr = edge_data.get((u, 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) @@ -817,49 +824,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. @@ -1095,6 +1149,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. @@ -3228,6 +3300,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. From a6dbfeb85ef3066cb05605617bd838d0e23ec36a Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Sun, 26 May 2024 16:47:09 +0200 Subject: [PATCH 11/21] Update cell_network.py --- src/compas/datastructures/cell_network/cell_network.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index bc7f8358d12..47431dbe386 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -3,6 +3,7 @@ from __future__ import division from __future__ import print_function +from ast import literal_eval from random import sample from compas.datastructures import Graph @@ -182,7 +183,7 @@ def __data__(self): "edge": self._edge, "face": self._face, "cell": cell, - "edge_data": self._edge_data, + "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, @@ -208,12 +209,10 @@ def __from_data__(cls, data): for key, attr in iter(vertex.items()): cell_network.add_vertex(key=int(key), attr_dict=attr) - edge_data = data.get("edge_data", {}) - print(edge_data) + edge_data = {literal_eval(k) : v for k, v in data.get("edge_data", {}).items()} for u in edge: for v in edge[u]: - print(">>", u, v) - attr = edge_data.get((u, v), {}) + 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 {} From c3c01ec44ee7a5b4b8aa8fc2cc772830d1e1128c Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Sun, 26 May 2024 16:48:32 +0200 Subject: [PATCH 12/21] Delete cell_network copy.py --- .../cell_network/cell_network copy.py | 4307 ----------------- 1 file changed, 4307 deletions(-) delete mode 100644 src/compas/datastructures/cell_network/cell_network copy.py diff --git a/src/compas/datastructures/cell_network/cell_network copy.py b/src/compas/datastructures/cell_network/cell_network copy.py deleted file mode 100644 index 67bc4e7a7a9..00000000000 --- a/src/compas/datastructures/cell_network/cell_network copy.py +++ /dev/null @@ -1,4307 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from random import sample - -from compas.datastructures import Graph -from compas.datastructures import Mesh -from compas.datastructures.attributes import CellAttributeView -from compas.datastructures.attributes import EdgeAttributeView -from compas.datastructures.attributes import FaceAttributeView -from compas.datastructures.attributes import VertexAttributeView -from compas.datastructures.datastructure import Datastructure -from compas.files import OBJ -from compas.geometry import Line -from compas.geometry import Point -from compas.geometry import Polygon -from compas.geometry import Polyhedron -from compas.geometry import Vector -from compas.geometry import add_vectors -from compas.geometry import bestfit_plane -from compas.geometry import bounding_box -from compas.geometry import centroid_points -from compas.geometry import centroid_polygon -from compas.geometry import centroid_polyhedron -from compas.geometry import distance_point_point -from compas.geometry import length_vector -from compas.geometry import normal_polygon -from compas.geometry import normalize_vector -from compas.geometry import project_point_plane -from compas.geometry import scale_vector -from compas.geometry import subtract_vectors -from compas.geometry import volume_polyhedron -from compas.tolerance import TOL -from compas.utilities import pairwise - - -class CellNetwork(Datastructure): - """Geometric implementation of a data structure for a collection of mixed topologic entities such as cells, faces, edges and nodes. - - Parameters - ---------- - default_vertex_attributes: dict, optional - Default values for vertex attributes. - default_edge_attributes: dict, optional - Default values for edge attributes. - default_face_attributes: dict, optional - Default values for face attributes. - default_cell_attributes: dict, optional - Default values for cell attributes. - name : str, optional - The name of the cell network. - **kwargs : dict, optional - Additional keyword arguments, which are stored in the attributes dict. - - Attributes - ---------- - default_vertex_attributes : dict[str, Any] - Default attributes of the vertices. - default_edge_attributes: dict[str, Any] - Default values for edge attributes. - default_face_attributes: dict[str, Any] - Default values for face attributes. - default_cell_attributes: dict[str, Any] - Default values for cell attributes. - - Examples - -------- - >>> 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]] - >>> [network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] - >>> [cell_network.add_face(fverts) for fverts in faces] - >>> [cell_network.add_cell(fkeys) for fkeys in cells] - >>> cell_network - - """ - - DATASCHEMA = { - "type": "object", - "properties": { - "attributes": {"type": "object"}, - "default_vertex_attributes": {"type": "object"}, - "default_edge_attributes": {"type": "object"}, - "default_face_attributes": {"type": "object"}, - "default_cell_attributes": {"type": "object"}, - "vertex": { - "type": "object", - "patternProperties": {"^[0-9]+$": {"type": "object"}}, - "additionalProperties": False, - }, - "edge": { - "type": "object", - "patternProperties": { - "^[0-9]+$": { - "type": "object", - "patternProperties": {"^[0-9]+$": {"type": "object"}}, - "additionalProperties": False, - } - }, - "additionalProperties": False, - }, - "face": { - "type": "object", - "patternProperties": { - "^[0-9]+$": { - "type": "array", - "items": {"type": "integer", "minimum": 0}, - "minItems": 3, - } - }, - "additionalProperties": False, - }, - "cell": { - "type": "object", - "patternProperties": { - "^[0-9]+$": { - "type": "array", - "minItems": 4, - "items": { - "type": "array", - "minItems": 3, - "items": {"type": "integer", "minimum": 0}, - }, - } - }, - "additionalProperties": False, - }, - "face_data": { - "type": "object", - "patternProperties": {"^\\([0-9]+(, [0-9]+){3, }\\)$": {"type": "object"}}, - "additionalProperties": False, - }, - "cell_data": { - "type": "object", - "patternProperties": {"^[0-9]+$": {"type": "object"}}, - "additionalProperties": False, - }, - "max_vertex": {"type": "number", "minimum": -1}, - "max_face": {"type": "number", "minimum": -1}, - "max_cell": {"type": "number", "minimum": -1}, - }, - "required": [ - "attributes", - "default_vertex_attributes", - "default_edge_attributes", - "default_face_attributes", - "default_cell_attributes", - "vertex", - "edge", - "face", - "cell", - "face_data", - "cell_data", - "max_vertex", - "max_face", - "max_cell", - ], - } - - @property - def __data__(self): - cell = {} - for c in self._cell: - faces = set() - for u in self._cell[c]: - for v in self._cell[c][u]: - faces.add(self._cell[c][u][v]) - cell[c] = sorted(list(faces)) - - return { - "attributes": self.attributes, - "default_vertex_attributes": self.default_vertex_attributes, - "default_edge_attributes": self.default_edge_attributes, - "default_face_attributes": self.default_face_attributes, - "default_cell_attributes": self.default_cell_attributes, - "vertex": self._vertex, - "edge": self._edge, - "face": self._face, - "cell": cell, - "face_data": self._face_data, - "cell_data": self._cell_data, - "max_vertex": self._max_vertex, - "max_face": self._max_face, - "max_cell": self._max_cell, - } - - @classmethod - def __from_data__(cls, data): - cell_network = cls( - default_vertex_attributes=data.get("default_vertex_attributes"), - default_edge_attributes=data.get("default_edge_attributes"), - default_face_attributes=data.get("default_face_attributes"), - default_cell_attributes=data.get("default_cell_attributes"), - ) - cell_network.attributes.update(data.get("attributes") or {}) - - vertex = data["vertex"] or {} - edge = data["edge"] or {} - face = data["face"] or {} - cell = data["cell"] or {} - - for key, attr in iter(vertex.items()): - cell_network.add_vertex(key=key, attr_dict=attr) - - for u in edge: - for v, attr in edge[u].items(): - cell_network.add_edge(u, 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_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._max_vertex = data.get("max_vertex", cell_network._max_vertex) - cell_network._max_face = data.get("max_face", cell_network._max_face) - cell_network._max_cell = data.get("max_cell", cell_network._max_cell) - - return cell_network - - def __init__(self, default_vertex_attributes=None, default_edge_attributes=None, default_face_attributes=None, default_cell_attributes=None, name=None, **kwargs): - super(CellNetwork, self).__init__(kwargs, name=name) - self._max_vertex = -1 - self._max_face = -1 - self._max_cell = -1 - self._vertex = {} - self._edge = {} - 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} - self.default_edge_attributes = {} - self.default_face_attributes = {} - self.default_cell_attributes = {} - if default_vertex_attributes: - self.default_vertex_attributes.update(default_vertex_attributes) - if default_edge_attributes: - self.default_edge_attributes.update(default_edge_attributes) - if default_face_attributes: - self.default_face_attributes.update(default_face_attributes) - if default_cell_attributes: - self.default_cell_attributes.update(default_cell_attributes) - - def __str__(self): - tpl = "" - return tpl.format( - self.number_of_vertices(), - self.number_of_faces(), - self.number_of_cells(), - self.number_of_edges(), - ) - - # -------------------------------------------------------------------------- - # Data - # -------------------------------------------------------------------------- - - @property - def data(self): - """Returns a dictionary of structured data representing the cell network data object. - - Note that some of the data stored internally in the data structure object is not included in the dictionary representation of the object. - This is the case for data that is considered private and/or redundant. - Specifically, the plane dictionary are not included. - This is because the information in these dictionaries can be reconstructed from the other data. - Therefore, to keep the dictionary representation as compact as possible, these dictionaries are not included. - - Returns - ------- - dict - The structured data representing the cell network. - - """ - cell = {} - for c in self._cell: - faces = set() - for u in self._cell[c]: - for v in self._cell[c][u]: - faces.add(self._cell[c][u][v]) - cell[c] = sorted(list(faces)) - - return { - "attributes": self.attributes, - "dva": self.default_vertex_attributes, - "dea": self.default_edge_attributes, - "dfa": self.default_face_attributes, - "dca": self.default_cell_attributes, - "vertex": self._vertex, - "edge": self._edge, - "face": self._face, - "cell": cell, - "edge_data": self._edge_data, - "face_data": self._face_data, - "cell_data": self._cell_data, - "max_vertex": self._max_vertex, - "max_face": self._max_face, - "max_cell": self._max_cell, - } - - @classmethod - def from_data(cls, data): - dva = data.get("dva") or {} - dea = data.get("dea") or {} - dfa = data.get("dfa") or {} - dca = data.get("dca") or {} - - cell_network = cls( - default_vertex_attributes=dva, - default_edge_attributes=dea, - default_face_attributes=dfa, - default_cell_attributes=dca, - ) - - cell_network.attributes.update(data.get("attributes") or {}) - - vertex = data.get("vertex") or {} - edge = data.get("edge") or {} - face = data.get("face") or {} - cell = data.get("cell") or {} - - for key, attr in iter(vertex.items()): - cell_network.add_vertex(key=key, attr_dict=attr) - - edge_data = data.get("edge_data") or {} - for u in edge: - for v in edge[u]: - attr = edge_data.get(tuple(sorted((u, v))), {}) - cell_network.add_edge(u, v, attr_dict=attr) - - face_data = data.get("face_data") or {} - for key, vertices in iter(face.items()): - attr = face_data.get(key) or {} - cell_network.add_face(vertices, fkey=key, attr_dict=attr) - - cell_data = data.get("cell_data") or {} - for ckey, faces in iter(cell.items()): - attr = cell_data.get(ckey) or {} - cell_network.add_cell(faces, ckey=ckey, attr_dict=attr) - - cell_network._max_vertex = data.get("max_vertex", cell_network._max_vertex) - cell_network._max_face = data.get("max_face", cell_network._max_face) - cell_network._max_cell = data.get("max_cell", cell_network._max_cell) - - return cell_network - - # -------------------------------------------------------------------------- - # Helpers - # -------------------------------------------------------------------------- - - def clear(self): - """Clear all the volmesh data. - - Returns - ------- - None - - """ - del self._vertex - del self._edge - del self._face - del self._cell - del self._plane - del self._face_data - del self._cell_data - self._vertex = {} - self._edge = {} - self._face = {} - self._cell = {} - self._plane = {} - self._face_data = {} - self._cell_data = {} - self._max_vertex = -1 - self._max_face = -1 - self._max_cell = -1 - - def vertex_sample(self, size=1): - """Get the identifiers of a set of random vertices. - - Parameters - ---------- - size : int, optional - The size of the sample. - - Returns - ------- - list[int] - The identifiers of the vertices. - - See Also - -------- - :meth:`edge_sample`, :meth:`face_sample`, :meth:`cell_sample` - - """ - return sample(list(self.vertices()), size) - - def edge_sample(self, size=1): - """Get the identifiers of a set of random edges. - - Parameters - ---------- - size : int, optional - The size of the sample. - - Returns - ------- - list[tuple[int, int]] - The identifiers of the edges. - - See Also - -------- - :meth:`vertex_sample`, :meth:`face_sample`, :meth:`cell_sample` - - """ - return sample(list(self.edges()), size) - - def face_sample(self, size=1): - """Get the identifiers of a set of random faces. - - Parameters - ---------- - size : int, optional - The size of the sample. - - Returns - ------- - list[int] - The identifiers of the faces. - - See Also - -------- - :meth:`vertex_sample`, :meth:`edge_sample`, :meth:`cell_sample` - - """ - return sample(list(self.faces()), size) - - def cell_sample(self, size=1): - """Get the identifiers of a set of random cells. - - Parameters - ---------- - size : int, optional - The size of the sample. - - Returns - ------- - list[int] - The identifiers of the cells. - - See Also - -------- - :meth:`vertex_sample`, :meth:`edge_sample`, :meth:`face_sample` - - """ - return sample(list(self.cells()), size) - - def vertex_index(self): - """Returns a dictionary that maps vertex identifiers to the corresponding index in a vertex list or array. - - Returns - ------- - dict[int, int] - A dictionary of vertex-index pairs. - - See Also - -------- - :meth:`index_vertex` - - """ - return {key: index for index, key in enumerate(self.vertices())} - - def index_vertex(self): - """Returns a dictionary that maps the indices of a vertex list to vertex identifiers. - - Returns - ------- - dict[int, int] - A dictionary of index-vertex pairs. - - See Also - -------- - :meth:`vertex_index` - - """ - return dict(enumerate(self.vertices())) - - def vertex_gkey(self, precision=None): - """Returns a dictionary that maps vertex identifiers to the corresponding *geometric key* up to a certain precision. - - Parameters - ---------- - precision : int, optional - Precision for converting numbers to strings. - Default is :attr:`TOL.precision`. - - Returns - ------- - dict[int, str] - A dictionary of vertex-geometric key pairs. - - See Also - -------- - :meth:`gkey_vertex` - - """ - gkey = TOL.geometric_key - xyz = self.vertex_coordinates - return {vertex: gkey(xyz(vertex), precision) for vertex in self.vertices()} - - def gkey_vertex(self, precision=None): - """Returns a dictionary that maps *geometric keys* of a certain precision to the corresponding vertex identifiers. - - Parameters - ---------- - precision : int, optional - Precision for converting numbers to strings. - Default is :attr:`TOL.precision`. - - Returns - ------- - dict[str, int] - A dictionary of geometric key-vertex pairs. - - See Also - -------- - :meth:`vertex_gkey` - - """ - gkey = TOL.geometric_key - xyz = self.vertex_coordinates - return {gkey(xyz(vertex), precision): vertex for vertex in self.vertices()} - - # -------------------------------------------------------------------------- - # Builders - # -------------------------------------------------------------------------- - - def add_vertex(self, key=None, attr_dict=None, **kwattr): - """Add a vertex and specify its attributes. - - Parameters - ---------- - key : int, optional - The identifier of the vertex. - Defaults to None. - attr_dict : dict, optional - A dictionary of vertex attributes. - Defaults to None. - **kwattr : dict, optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - int - The identifier of the vertex. - - See Also - -------- - :meth:`add_face`, :meth:`add_cell`, :meth:`add_edge` - - """ - if key is None: - key = self._max_vertex = self._max_vertex + 1 - key = int(key) - if key > self._max_vertex: - self._max_vertex = key - - if key not in self._vertex: - self._vertex[key] = {} - self._edge[key] = {} - self._plane[key] = {} - - attr = attr_dict or {} - attr.update(kwattr) - self._vertex[key].update(attr) - - return key - - def add_edge(self, u, v, attr_dict=None, **kwattr): - """Add an edge and specify its attributes. - - Parameters - ---------- - u : int - The identifier of the first node of the edge. - v : int - The identifier of the second node of the edge. - attr_dict : dict[str, Any], optional - A dictionary of edge attributes. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - tuple[int, int] - The identifier of the edge. - - Raises - ------ - ValueError - If either of the vertices of the edge does not exist. - - Notes - ----- - Edges can be added independently from faces or cells. - However, whenever a face is added all edges of that face are added as well. - - """ - if u not in self._vertex: - raise ValueError("Cannot add edge {}, {} has no vertex {}".format((u, v), self.name, u)) - if v not in self._vertex: - raise ValueError("Cannot add edge {}, {} has no vertex {}".format((u, v), self.name, u)) - - attr = attr_dict or {} - attr.update(kwattr) - - uv = tuple(sorted((u, v))) - - data = self._edge_data.get(uv, {}) - data.update(attr) - 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? - # 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._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 - - def add_face(self, vertices, fkey=None, attr_dict=None, **kwattr): - """Add a face to the cell network. - - Parameters - ---------- - vertices : list[int] - A list of ordered vertex keys representing the face. - For every vertex that does not yet exist, a new vertex is created. - fkey : int, optional - The face identifier. - attr_dict : dict[str, Any], optional - dictionary of halfface attributes. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - int - The key of the face. - - See Also - -------- - :meth:`add_vertex`, :meth:`add_cell`, :meth:`add_edge` - - Notes - ----- - If no key is provided for the face, one is generated - automatically. An automatically generated key is an integer that increments - the highest integer value of any key used so far by 1. - - If a key with an integer value is provided that is higher than the current - highest integer key value, then the highest integer value is updated accordingly. - - All edges of the faces are automatically added if they don't exsit yet. - The vertices of the face should form a continuous closed loop. - However, the cycle direction doesn't matter. - - """ - if len(vertices) < 3: - return - - 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 - - self._face[fkey] = vertices - - attr = attr_dict or {} - attr.update(kwattr) - for name, value in attr.items(): - self.face_attribute(fkey, name, value) - - for u, v in pairwise(vertices + vertices[:1]): - 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.add_edge(u, v) - - return fkey - - def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): - """Add a cell to the cell network object. - - In order to add a valid cell to the network, the faces must form a closed mesh. - If the faces do not form a closed mesh, the cell is not added to the network. - - Parameters - ---------- - faces : list[int] - The face keys of the cell. - ckey : int, optional - The cell identifier. - attr_dict : dict[str, Any], optional - A dictionary of cell attributes. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - int - The key of the cell. - - Raises - ------ - ValueError - If something is wrong with the passed faces. - TypeError - If the provided cell key is not an integer. - - Notes - ----- - If no key is provided for the cell, one is generated - automatically. An automatically generated key is an integer that increments - the highest integer value of any key used so far by 1. - - If a key with an integer value is provided that is higher than the current - 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 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: - raise ValueError("Cannot add cell, faces {} can not be unified.".format(faces)) - - # 3. Check if the faces are oriented correctly - # 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: - mesh.flip_cycles() - - if ckey is None: - ckey = self._max_cell = self._max_cell + 1 - ckey = int(ckey) - if ckey > self._max_cell: - self._max_cell = ckey - - self._cell[ckey] = {} - - attr = attr_dict or {} - attr.update(kwattr) - for name, value in attr.items(): - self.cell_attribute(ckey, name, value) - - for fkey in mesh.faces(): - vertices = mesh.face_vertices(fkey) - for u, v in pairwise(vertices + vertices[:1]): - if u not in self._cell[ckey]: - self._cell[ckey][u] = {} - self._plane[u][v][fkey] = ckey - self._cell[ckey][u][v] = fkey - - return ckey - - # -------------------------------------------------------------------------- - # Modifiers - # -------------------------------------------------------------------------- - - # def delete_vertex(self, vertex): - # """Delete a vertex from the cell network and everything that is attached to it. - - # Parameters - # ---------- - # vertex : int - # The identifier of the vertex. - - # Returns - # ------- - # None - - # See Also - # -------- - # :meth:`delete_halfface`, :meth:`delete_cell` - - # """ - # for cell in self.vertex_cells(vertex): - # self.delete_cell(cell) - - def delete_face(self, face): - vertices = self.face_vertices(face) - # check first - for u, v in pairwise(vertices + vertices[:1]): - if self._plane[u][v][face] != None: - print("Cannot delete face %d, delete cell %s first" % (face, self._plane[u][v][face])) - return - if self._plane[v][u][face] != None: - print("Cannot delete face %d, delete cell %s first" % (face, self._plane[u][v][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): - # 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 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` - - # """ - # 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 remove_unused_vertices(self): - # """Remove all unused vertices from the cell network object. - - # Returns - # ------- - # None - - # """ - # for vertex in list(self.vertices()): - # if vertex not in self._plane: - # del self._vertex[vertex] - # else: - # if not self._plane[vertex]: - # del self._vertex[vertex] - # del self._plane[vertex] - - # -------------------------------------------------------------------------- - # Constructors - # -------------------------------------------------------------------------- - - # @classmethod - # def from_meshgrid(cls, dx=10, dy=None, dz=None, nx=10, ny=None, nz=None): - # """Construct a cell network from a 3D meshgrid. - - # Parameters - # ---------- - # dx : float, optional - # The size of the grid in the x direction. - # dy : float, optional - # The size of the grid in the y direction. - # Defaults to the value of `dx`. - # dz : float, optional - # The size of the grid in the z direction. - # Defaults to the value of `dx`. - # nx : int, optional - # The number of elements in the x direction. - # ny : int, optional - # The number of elements in the y direction. - # Defaults to the value of `nx`. - # nz : int, optional - # The number of elements in the z direction. - # Defaults to the value of `nx`. - - # Returns - # ------- - # :class:`compas.datastructures.VolMesh` - - # See Also - # -------- - # :meth:`from_obj`, :meth:`from_vertices_and_cells` - - # """ - # dy = dy or dx - # dz = dz or dx - # ny = ny or nx - # nz = nz or nx - - # vertices = [ - # [x, y, z] - # for z, x, y in product( - # linspace(0, dz, nz + 1), - # linspace(0, dx, nx + 1), - # linspace(0, dy, ny + 1), - # ) - # ] - # cells = [] - # for k, i, j in product(range(nz), range(nx), range(ny)): - # a = k * ((nx + 1) * (ny + 1)) + i * (ny + 1) + j - # b = k * ((nx + 1) * (ny + 1)) + (i + 1) * (ny + 1) + j - # c = k * ((nx + 1) * (ny + 1)) + (i + 1) * (ny + 1) + j + 1 - # d = k * ((nx + 1) * (ny + 1)) + i * (ny + 1) + j + 1 - # aa = (k + 1) * ((nx + 1) * (ny + 1)) + i * (ny + 1) + j - # bb = (k + 1) * ((nx + 1) * (ny + 1)) + (i + 1) * (ny + 1) + j - # cc = (k + 1) * ((nx + 1) * (ny + 1)) + (i + 1) * (ny + 1) + j + 1 - # dd = (k + 1) * ((nx + 1) * (ny + 1)) + i * (ny + 1) + j + 1 - # bottom = [d, c, b, a] - # front = [a, b, bb, aa] - # right = [b, c, cc, bb] - # left = [a, aa, dd, d] - # back = [c, d, dd, cc] - # top = [aa, bb, cc, dd] - # cells.append([bottom, front, left, back, right, top]) - - # return cls.from_vertices_and_cells(vertices, cells) - - @classmethod - def from_obj(cls, filepath, precision=None): - """Construct a cell network object from the data described in an OBJ file. - - Parameters - ---------- - filepath : path string | file-like object | URL string - A path, a file-like object or a URL pointing to a file. - precision: str, optional - The precision of the geometric map that is used to connect the lines. - - Returns - ------- - :class:`compas.datastructures.VolMesh` - A cell network object. - - See Also - -------- - :meth:`to_obj` - :meth:`from_meshgrid`, :meth:`from_vertices_and_cells` - :class:`compas.files.OBJ` - - """ - 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) - - @classmethod - def from_vertices_and_cells(cls, vertices, cells): - """Construct a cell network object from vertices and cells. - - Parameters - ---------- - vertices : list[list[float]] - Ordered list of vertices, represented by their XYZ coordinates. - cells : list[list[list[int]]] - List of cells defined by their faces. - - Returns - ------- - :class:`compas.datastructures.VolMesh` - A cell network object. - - See Also - -------- - :meth:`to_vertices_and_cells` - :meth:`from_obj` - - """ - cellnetwork = cls() - for x, y, z in vertices: - cellnetwork.add_vertex(x=x, y=y, z=z) - for cell in cells: - faces = [] - for vertices in cell: - face = cellnetwork.add_face(vertices) - faces.append(face) - cellnetwork.add_cell(faces) - return cellnetwork - - # -------------------------------------------------------------------------- - # Conversions - # -------------------------------------------------------------------------- - - # def to_obj(self, filepath, precision=None, **kwargs): - # """Write the cell network to an OBJ file. - - # Parameters - # ---------- - # filepath : path string | file-like object - # A path or a file-like object pointing to a file. - # precision: str, optional - # The precision of the geometric map that is used to connect the lines. - # unweld : bool, optional - # If True, all faces have their own unique vertices. - # If False, vertices are shared between faces if this is also the case in the mesh. - # Default is False. - - # Returns - # ------- - # None - - # See Also - # -------- - # :meth:`from_obj` - - # Warnings - # -------- - # This function only writes geometric data about the vertices and - # the faces to the file. - - # """ - # obj = OBJ(filepath, precision=precision) - # obj.write(self, **kwargs) - - # def to_vertices_and_cells(self): - # """Return the vertices and cells of a cell network. - - # Returns - # ------- - # list[list[float]] - # A list of vertices, represented by their XYZ coordinates. - # list[list[list[int]]] - # A list of cells, with each cell a list of faces, and each face a list of vertex indices. - - # See Also - # -------- - # :meth:`from_vertices_and_cells` - - # """ - # vertex_index = self.vertex_index() - # vertices = [self.vertex_coordinates(vertex) for vertex in self.vertices()] - # cells = [] - # for cell in self.cells(): - # faces = [ - # [vertex_index[vertex] for vertex in self.halfface_vertices(face)] for face in self.cell_faces(cell) - # ] - # cells.append(faces) - # return vertices, cells - - def edges_to_graph(self): - """Convert the edges of the cell network to a graph. - - Returns - ------- - :class:`compas.datastructures.Graph` - A graph object. - - """ - graph = Graph() - for vertex, attr in self.vertices(data=True): - x, y, z = self.vertex_coordinates(vertex) - graph.add_node(key=vertex, x=x, y=y, z=z, attr_dict=attr) - for (u, v), attr in self.edges(data=True): - graph.add_edge(u, v, attr_dict=attr) - return graph - - def cell_to_vertices_and_faces(self, cell): - """Return the vertices and faces of a cell. - - Parameters - ---------- - cell : int - Identifier of the cell. - - Returns - ------- - list[list[float]] - A list of vertices, represented by their XYZ coordinates, - list[list[int]] - A list of faces, with each face a list of vertex indices. - - See Also - -------- - :meth:`cell_to_mesh` - - """ - vertices = self.cell_vertices(cell) - faces = self.cell_faces(cell) - vertex_index = {vertex: index for index, vertex in enumerate(vertices)} - vertices = [self.vertex_coordinates(vertex) for vertex in vertices] - faces = [] - for face in self.cell_faces(cell): - faces.append([vertex_index[vertex] for vertex in self.cell_face_vertices(cell, face)]) - return vertices, faces - - def cell_to_mesh(self, cell): - """Construct a mesh object from from a cell of a cell network. - - Parameters - ---------- - cell : int - Identifier of the cell. - - Returns - ------- - :class:`compas.datastructures.Mesh` - A mesh object. - - See Also - -------- - :meth:`cell_to_vertices_and_faces` - - """ - vertices, faces = self.cell_to_vertices_and_faces(cell) - return Mesh.from_vertices_and_faces(vertices, faces) - - def faces_to_mesh(self, faces, data=False): - """Construct a mesh from a list of faces. - - Parameters - ---------- - faces : list - A list of face identifiers. - - Returns - ------- - :class:`compas.datastructures.Mesh` - A mesh. - - """ - faces_vertices = [self.face_vertices(face) for face in faces] - mesh = Mesh() - for fkey, vertices in zip(faces, faces_vertices): - for v in vertices: - x, y, z = self.vertex_coordinates(v) - mesh.add_vertex(key=v, x=x, y=y, z=z) - if data: - mesh.add_face(vertices, fkey=fkey, attr_dict=self.face_attributes(fkey)) - else: - mesh.add_face(vertices, fkey=fkey) - return mesh - - # -------------------------------------------------------------------------- - # General - # -------------------------------------------------------------------------- - - def centroid(self): - """Compute the centroid of the cell network. - - Returns - ------- - :class:`compas.geometry.Point` - The point at the centroid. - - """ - return Point(*centroid_points([self.vertex_coordinates(vertex) for vertex in self.vertices()])) - - def aabb(self): - """Calculate the axis aligned bounding box of the mesh. - - Returns - ------- - list[[float, float, float]] - XYZ coordinates of 8 points defining a box. - - """ - xyz = self.vertices_attributes("xyz") - return bounding_box(xyz) - - def number_of_vertices(self): - """Count the number of vertices in the cell network. - - Returns - ------- - int - The number of vertices. - - See Also - -------- - :meth:`number_of_edges`, :meth:`number_of_faces`, :meth:`number_of_cells` - - """ - return len(list(self.vertices())) - - def number_of_edges(self): - """Count the number of edges in the cell network. - - Returns - ------- - int - The number of edges. - - See Also - -------- - :meth:`number_of_vertices`, :meth:`number_of_faces`, :meth:`number_of_cells` - - """ - return len(list(self.edges())) - - def number_of_faces(self): - """Count the number of faces in the cell network. - - Returns - ------- - int - The number of faces. - - See Also - -------- - :meth:`number_of_vertices`, :meth:`number_of_edges`, :meth:`number_of_cells` - - """ - return len(list(self.faces())) - - def number_of_cells(self): - """Count the number of faces in the cell network. - - Returns - ------- - int - The number of cells. - - See Also - -------- - :meth:`number_of_vertices`, :meth:`number_of_edges`, :meth:`number_of_faces` - - """ - return len(list(self.cells())) - - def is_valid(self): - """Verify that the cell network is valid. - - Returns - ------- - bool - True if the cell network is valid. - False otherwise. - - """ - raise NotImplementedError - - # -------------------------------------------------------------------------- - # Vertex Accessors - # -------------------------------------------------------------------------- - - def vertices(self, data=False): - """Iterate over the vertices of the cell network. - - Parameters - ---------- - data : bool, optional - If True, yield the vertex attributes in addition to the vertex identifiers. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next vertex identifier. - If `data` is True, the next vertex as a (vertex, attr) a tuple. - - See Also - -------- - :meth:`edges`, :meth:`faces`, :meth:`cells` - - """ - for vertex in self._vertex: - if not data: - yield vertex - else: - yield vertex, self.vertex_attributes(vertex) - - def vertices_where(self, conditions=None, data=False, **kwargs): - """Get vertices for which a certain condition or set of conditions is true. - - Parameters - ---------- - conditions : dict, optional - A set of conditions in the form of key-value pairs. - The keys should be attribute names. The values can be attribute - values or ranges of attribute values in the form of min/max pairs. - data : bool, optional - If True, yield the vertex attributes in addition to the identifiers. - **kwargs : dict[str, Any], optional - Additional conditions provided as named function arguments. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next vertex that matches the condition. - If `data` is True, the next vertex and its attributes. - - See Also - -------- - :meth:`vertices_where_predicate` - :meth:`edges_where`, :meth:`faces_where`, :meth:`cells_where` - - """ - conditions = conditions or {} - conditions.update(kwargs) - - for key, attr in self.vertices(True): - is_match = True - - attr = attr or {} - - for name, value in conditions.items(): - method = getattr(self, name, None) - - if callable(method): - val = method(key) - - if isinstance(val, list): - if value not in val: - is_match = False - break - break - - if isinstance(value, (tuple, list)): - minval, maxval = value - if val < minval or val > maxval: - is_match = False - break - else: - if value != val: - is_match = False - break - - else: - if name not in attr: - is_match = False - break - - if isinstance(attr[name], list): - if value not in attr[name]: - is_match = False - break - break - - if isinstance(value, (tuple, list)): - minval, maxval = value - if attr[name] < minval or attr[name] > maxval: - is_match = False - break - else: - if value != attr[name]: - is_match = False - break - - if is_match: - if data: - yield key, attr - else: - yield key - - def vertices_where_predicate(self, predicate, data=False): - """Get vertices for which a certain condition or set of conditions is true using a lambda function. - - Parameters - ---------- - predicate : callable - The condition you want to evaluate. - The callable takes 2 parameters: the vertex identifier and the vertex attributes, and should return True or False. - data : bool, optional - If True, yield the vertex attributes in addition to the identifiers. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next vertex that matches the condition. - If `data` is True, the next vertex and its attributes. - - See Also - -------- - :meth:`vertices_where` - :meth:`edges_where_predicate`, :meth:`faces_where_predicate`, :meth:`cells_where_predicate` - - """ - for key, attr in self.vertices(True): - if predicate(key, attr): - if data: - yield key, attr - else: - yield key - - # -------------------------------------------------------------------------- - # Vertex Attributes - # -------------------------------------------------------------------------- - - def update_default_vertex_attributes(self, attr_dict=None, **kwattr): - """Update the default vertex attributes. - - Parameters - ---------- - attr_dict : dict[str, Any], optional - A dictionary of attributes with their default values. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - None - - See Also - -------- - :meth:`update_default_edge_attributes`, :meth:`update_default_face_attributes`, :meth:`update_default_cell_attributes` - - Notes - ----- - Named arguments overwrite correpsonding name-value pairs in the attribute dictionary. - - """ - if not attr_dict: - attr_dict = {} - attr_dict.update(kwattr) - self.default_vertex_attributes.update(attr_dict) - - def vertex_attribute(self, vertex, name, value=None): - """Get or set an attribute of a vertex. - - Parameters - ---------- - vertex : int - The vertex identifier. - name : str - The name of the attribute - value : object, optional - The value of the attribute. - - Returns - ------- - object | None - The value of the attribute, - or None when the function is used as a "setter". - - Raises - ------ - KeyError - If the vertex does not exist. - - See Also - -------- - :meth:`unset_vertex_attribute` - :meth:`vertex_attributes`, :meth:`vertices_attribute`, :meth:`vertices_attributes` - :meth:`edge_attribute`, :meth:`face_attribute`, :meth:`cell_attribute` - - """ - if vertex not in self._vertex: - raise KeyError(vertex) - if value is not None: - self._vertex[vertex][name] = value - return None - if name in self._vertex[vertex]: - return self._vertex[vertex][name] - else: - if name in self.default_vertex_attributes: - return self.default_vertex_attributes[name] - - def unset_vertex_attribute(self, vertex, name): - """Unset the attribute of a vertex. - - Parameters - ---------- - vertex : int - The vertex identifier. - name : str - The name of the attribute. - - Returns - ------- - None - - Raises - ------ - KeyError - If the vertex does not exist. - - See Also - -------- - :meth:`vertex_attribute` - - Notes - ----- - Unsetting the value of a vertex attribute implicitly sets it back to the value - stored in the default vertex attribute dict. - - """ - if name in self._vertex[vertex]: - del self._vertex[vertex][name] - - def vertex_attributes(self, vertex, names=None, values=None): - """Get or set multiple attributes of a vertex. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - names : list[str], optional - A list of attribute names. - values : list[Any], optional - A list of attribute values. - - Returns - ------- - dict[str, Any] | list[Any] | None - If the parameter `names` is empty, - the function returns a dictionary of all attribute name-value pairs of the vertex. - If the parameter `names` is not empty, - the function returns a list of the values corresponding to the requested attribute names. - The function returns None if it is used as a "setter". - - Raises - ------ - KeyError - If the vertex does not exist. - - See Also - -------- - :meth:`vertex_attribute`, :meth:`vertices_attribute`, :meth:`vertices_attributes` - :meth:`edge_attributes`, :meth:`face_attributes`, :meth:`cell_attributes` - - """ - if vertex not in self._vertex: - raise KeyError(vertex) - if names and values is not None: - # use it as a setter - for name, value in zip(names, values): - self._vertex[vertex][name] = value - return - # use it as a getter - if not names: - # return all vertex attributes as a dict - return VertexAttributeView(self.default_vertex_attributes, self._vertex[vertex]) - values = [] - for name in names: - if name in self._vertex[vertex]: - values.append(self._vertex[vertex][name]) - elif name in self.default_vertex_attributes: - values.append(self.default_vertex_attributes[name]) - else: - values.append(None) - return values - - def vertices_attribute(self, name, value=None, keys=None): - """Get or set an attribute of multiple vertices. - - Parameters - ---------- - name : str - The name of the attribute. - value : object, optional - The value of the attribute. - Default is None. - keys : list[int], optional - A list of vertex identifiers. - - Returns - ------- - list[Any] | None - The value of the attribute for each vertex, - or None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the vertices does not exist. - - See Also - -------- - :meth:`vertex_attribute`, :meth:`vertex_attributes`, :meth:`vertices_attributes` - :meth:`edges_attribute`, :meth:`faces_attribute`, :meth:`cells_attribute` - - """ - vertices = keys or self.vertices() - if value is not None: - for vertex in vertices: - self.vertex_attribute(vertex, name, value) - return - return [self.vertex_attribute(vertex, name) for vertex in vertices] - - def vertices_attributes(self, names=None, values=None, keys=None): - """Get or set multiple attributes of multiple vertices. - - Parameters - ---------- - names : list[str], optional - The names of the attribute. - Default is None. - values : list[Any], optional - The values of the attributes. - Default is None. - key : list[Any], optional - A list of vertex identifiers. - - Returns - ------- - list[dict[str, Any]] | list[list[Any]] | None - If the parameter `names` is empty, - the function returns a list containing an attribute dict per vertex. - If the parameter `names` is not empty, - the function returns a list containing a list of attribute values per vertex corresponding to the provided attribute names. - The function returns None if it is used as a "setter". - - Raises - ------ - KeyError - If any of the vertices does not exist. - - See Also - -------- - :meth:`vertex_attribute`, :meth:`vertex_attributes`, :meth:`vertices_attribute` - :meth:`edges_attributes`, :meth:`faces_attributes`, :meth:`cells_attributes` - - """ - vertices = keys or self.vertices() - if values: - for vertex in vertices: - self.vertex_attributes(vertex, names, values) - return - return [self.vertex_attributes(vertex, names) for vertex in vertices] - - # -------------------------------------------------------------------------- - # Vertex Topology - # -------------------------------------------------------------------------- - - def has_vertex(self, vertex): - """Verify that a vertex is in the cell network. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - - Returns - ------- - bool - True if the vertex is in the cell network. - False otherwise. - - See Also - -------- - :meth:`has_edge`, :meth:`has_face`, :meth:`has_cell` - - """ - return vertex in self._vertex - - def vertex_neighbors(self, vertex): - """Return the vertex neighbors of a vertex. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - - Returns - ------- - list[int] - The list of neighboring vertices. - - See Also - -------- - :meth:`vertex_degree`, :meth:`vertex_min_degree`, :meth:`vertex_max_degree` - :meth:`vertex_faces`, :meth:`vertex_halffaces`, :meth:`vertex_cells` - :meth:`vertex_neighborhood` - - """ - return self._edge[vertex].keys() - - def vertex_neighborhood(self, vertex, ring=1): - """Return the vertices in the neighborhood of a vertex. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - ring : int, optional - The number of neighborhood rings to include. - - Returns - ------- - list[int] - The vertices in the neighborhood. - - See Also - -------- - :meth:`vertex_neighbors` - - Notes - ----- - The vertices in the neighborhood are unordered. - - """ - nbrs = set(self.vertex_neighbors(vertex)) - i = 1 - while True: - if i == ring: - break - temp = [] - for nbr in nbrs: - temp += self.vertex_neighbors(nbr) - nbrs.update(temp) - i += 1 - return list(nbrs - set([vertex])) - - def vertex_degree(self, vertex): - """Count the neighbors of a vertex. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - - Returns - ------- - int - The degree of the vertex. - - See Also - -------- - :meth:`vertex_neighbors`, :meth:`vertex_min_degree`, :meth:`vertex_max_degree` - - """ - return len(self.vertex_neighbors(vertex)) - - def vertex_min_degree(self): - """Compute the minimum degree of all vertices. - - Returns - ------- - int - The lowest degree of all vertices. - - See Also - -------- - :meth:`vertex_degree`, :meth:`vertex_max_degree` - - """ - if not self._vertex: - return 0 - return min(self.vertex_degree(vertex) for vertex in self.vertices()) - - def vertex_max_degree(self): - """Compute the maximum degree of all vertices. - - Returns - ------- - int - The highest degree of all vertices. - - See Also - -------- - :meth:`vertex_degree`, :meth:`vertex_min_degree` - - """ - if not self._vertex: - return 0 - return max(self.vertex_degree(vertex) for vertex in self.vertices()) - - def vertex_faces(self, vertex): - """Return all faces connected to a vertex. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - - Returns - ------- - list[int] - The list of faces connected to a vertex. - - See Also - -------- - :meth:`vertex_neighbors`, :meth:`vertex_cells` - - """ - faces = [] - for nbr in self._plane[vertex]: - for face in self._plane[vertex][nbr]: - if face is not None: - faces.append(face) - return faces - - def vertex_cells(self, vertex): - """Return all cells connected to a vertex. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - - Returns - ------- - list[int] - The list of cells connected to a vertex. - - See Also - -------- - :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(): - if cell is not None: - cells.add(cell) - return list(cells) - - # def is_vertex_on_boundary(self, vertex): - # """Verify that a vertex is on a boundary. - - # Parameters - # ---------- - # vertex : int - # The identifier of the vertex. - - # Returns - # ------- - # bool - # True if the vertex is on the boundary. - # False otherwise. - - # See Also - # -------- - # :meth:`is_edge_on_boundary`, :meth:`is_face_on_boundary`, :meth:`is_cell_on_boundary` - - # """ - # halffaces = self.vertex_halffaces(vertex) - # for halfface in halffaces: - # if self.is_halfface_on_boundary(halfface): - # return True - # return False - - # -------------------------------------------------------------------------- - # Vertex Geometry - # -------------------------------------------------------------------------- - - def vertex_coordinates(self, vertex, axes="xyz"): - """Return the coordinates of a vertex. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - axes : str, optional - The axes alon which to take the coordinates. - Should be a combination of x, y, and z. - - Returns - ------- - list[float] - Coordinates of the vertex. - """ - return [self._vertex[vertex][axis] for axis in axes] - - def vertices_coordinates(self, vertices, axes="xyz"): - """Return the coordinates of multiple vertices. - - Parameters - ---------- - vertices : list of int - The vertex identifiers. - axes : str, optional - The axes alon which to take the coordinates. - Should be a combination of x, y, and z. - - Returns - ------- - list of list[float] - Coordinates of the vertices. - """ - return [self.vertex_coordinates(vertex, axes=axes) for vertex in vertices] - - def vertex_point(self, vertex): - """Return the point representation of a vertex. - - Parameters - ---------- - vertex : int - The identifier of the vertex. - - Returns - ------- - :class:`compas.geometry.Point` - The point. - """ - return Point(*self.vertex_coordinates(vertex)) - - def vertices_points(self, vertices): - """Returns the point representation of multiple vertices. - - Parameters - ---------- - vertices : list of int - The vertex identifiers. - - Returns - ------- - list of :class:`compas.geometry.Point` - The points. - """ - return [self.vertex_point(vertex) for vertex in vertices] - - # -------------------------------------------------------------------------- - # Edge Accessors - # -------------------------------------------------------------------------- - - def edges(self, data=False): - """Iterate over the edges of the cell network. - - Parameters - ---------- - data : bool, optional - If True, yield the edge attributes in addition to the edge identifiers. - - Yields - ------ - tuple[int, int] | tuple[tuple[int, int], dict[str, Any]] - If `data` is False, the next edge identifier (u, v). - If `data` is True, the next edge identifier and its attributes as a ((u, v), attr) tuple. - - """ - seen = set() - for u, nbrs in iter(self._edge.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 - - def edges_where(self, conditions=None, data=False, **kwargs): - """Get edges for which a certain condition or set of conditions is true. - - Parameters - ---------- - conditions : dict, optional - A set of conditions in the form of key-value pairs. - The keys should be attribute names. The values can be attribute - values or ranges of attribute values in the form of min/max pairs. - data : bool, optional - If True, yield the edge attributes in addition to the identifiers. - **kwargs : dict[str, Any], optional - Additional conditions provided as named function arguments. - - Yields - ------ - tuple[int, int] | tuple[tuple[int, int], dict[str, Any]] - If `data` is False, the next edge as a (u, v) tuple. - If `data` is True, the next edge as a (u, v, data) tuple. - - See Also - -------- - :meth:`edges_where_predicate` - :meth:`vertices_where`, :meth:`faces_where`, :meth:`cells_where` - - """ - conditions = conditions or {} - conditions.update(kwargs) - - for key in self.edges(): - is_match = True - - attr = self.edge_attributes(key) or {} - - for name, value in conditions.items(): - method = getattr(self, name, None) - - if method and callable(method): - val = method(key) - elif name in attr: - val = attr[name] - else: - is_match = False - break - - if isinstance(val, list): - if value not in val: - is_match = False - break - elif isinstance(value, (tuple, list)): - minval, maxval = value - if val < minval or val > maxval: - is_match = False - break - else: - if value != val: - is_match = False - break - - if is_match: - if data: - yield key, attr - else: - yield key - - def edges_where_predicate(self, predicate, data=False): - """Get edges for which a certain condition or set of conditions is true using a lambda function. - - Parameters - ---------- - predicate : callable - The condition you want to evaluate. - The callable takes 2 parameters: the edge identifier and the edge attributes, and should return True or False. - data : bool, optional - If True, yield the edge attributes in addition to the identifiers. - - Yields - ------ - tuple[int, int] | tuple[tuple[int, int], dict[str, Any]] - If `data` is False, the next edge as a (u, v) tuple. - If `data` is True, the next edge as a (u, v, data) tuple. - - See Also - -------- - :meth:`edges_where` - :meth:`vertices_where_predicate`, :meth:`faces_where_predicate`, :meth:`cells_where_predicate` - - """ - for key, attr in self.edges(True): - if predicate(key, attr): - if data: - yield key, attr - else: - yield key - - # -------------------------------------------------------------------------- - # Edge Attributes - # -------------------------------------------------------------------------- - - def update_default_edge_attributes(self, attr_dict=None, **kwattr): - """Update the default edge attributes. - - Parameters - ---------- - attr_dict : dict[str, Any], optional - A dictionary of attributes with their default values. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - None - - See Also - -------- - :meth:`update_default_vertex_attributes`, :meth:`update_default_face_attributes`, :meth:`update_default_cell_attributes` - - Notes - ----- - Named arguments overwrite correpsonding key-value pairs in the attribute dictionary. - - """ - if not attr_dict: - attr_dict = {} - attr_dict.update(kwattr) - self.default_edge_attributes.update(attr_dict) - - def edge_attribute(self, edge, name, value=None): - """Get or set an attribute of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - name : str - The name of the attribute. - value : object, optional - The value of the attribute. - - Returns - ------- - object | None - The value of the attribute, or None when the function is used as a "setter". - - Raises - ------ - KeyError - If the edge does not exist. - """ - if not self.has_edge(edge): - raise KeyError(edge) - - attr = self._edge_data.get(tuple(sorted(edge)), {}) - - if value is not None: - attr.update({name: value}) - self._edge_data[tuple(sorted(edge))] = attr - return - if name in attr: - return attr[name] - if name in self.default_edge_attributes: - return self.default_edge_attributes[name] - - def unset_edge_attribute(self, edge, name): - """Unset the attribute of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - name : str - The name of the attribute. - - Raises - ------ - KeyError - If the edge does not exist. - - Returns - ------- - None - - See Also - -------- - :meth:`edge_attribute` - - Notes - ----- - Unsetting the value of an edge attribute implicitly sets it back to the value - stored in the default edge attribute dict. - - """ - if not self.has_edge(edge): - raise KeyError(edge) - - 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. - - Parameters - ---------- - edge : tuple[int, int] - The identifier of the edge. - names : list[str], optional - A list of attribute names. - values : list[Any], optional - A list of attribute values. - - Returns - ------- - dict[str, Any] | list[Any] | None - If the parameter `names` is empty, a dictionary of all attribute name-value pairs of the edge. - If the parameter `names` is not empty, a list of the values corresponding to the provided names. - None if the function is used as a "setter". - - Raises - ------ - KeyError - If the edge does not exist. - - See Also - -------- - :meth:`edge_attribute`, :meth:`edges_attribute`, :meth:`edges_attributes` - :meth:`vertex_attributes`, :meth:`face_attributes`, :meth:`cell_attributes` - - """ - if not self.has_edge(edge): - raise KeyError(edge) - - if names and values: - for name, value in zip(names, values): - self._edge_data[tuple(sorted(edge))][name] = value - return - if not names: - return EdgeAttributeView(self.default_edge_attributes, self._edge_data[tuple(sorted(edge))]) - values = [] - for name in names: - value = self.edge_attribute(edge, name) - values.append(value) - return values - - def edges_attribute(self, name, value=None, edges=None): - """Get or set an attribute of multiple edges. - - Parameters - ---------- - name : str - The name of the attribute. - value : object, optional - The value of the attribute. - Default is None. - edges : list[tuple[int, int]], optional - A list of edge identifiers. - - Returns - ------- - list[Any] | None - A list containing the value per edge of the requested attribute, - or None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the edges does not exist. - - See Also - -------- - :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attributes` - :meth:`vertex_attribute`, :meth:`face_attribute`, :meth:`cell_attribute` - - """ - edges = edges or self.edges() - if value is not None: - for edge in edges: - self.edge_attribute(edge, name, value) - return - return [self.edge_attribute(edge, name) for edge in edges] - - def edges_attributes(self, names=None, values=None, edges=None): - """Get or set multiple attributes of multiple edges. - - Parameters - ---------- - names : list[str], optional - The names of the attribute. - values : list[Any], optional - The values of the attributes. - edges : list[tuple[int, int]], optional - A list of edge identifiers. - - Returns - ------- - list[dict[str, Any]] | list[list[Any]] | None - If the parameter `names` is empty, - a list containing per edge an attribute dict with all attributes (default + custom) of the edge. - If the parameter `names` is not empty, - a list containing per edge a list of attribute values corresponding to the requested names. - None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the edges does not exist. - - See Also - -------- - :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute` - :meth:`vertex_attributes`, :meth:`face_attributes`, :meth:`cell_attributes` - - """ - edges = edges or self.edges() - if values: - for edge in edges: - self.edge_attributes(edge, names, values) - return - return [self.edge_attributes(edge, names) for edge in edges] - - # -------------------------------------------------------------------------- - # Edge Topology - # -------------------------------------------------------------------------- - - def has_edge(self, edge, directed=False): - """Verify that the cell network contains a directed edge (u, v). - - Parameters - ---------- - edge : tuple[int, int] - The identifier of the edge. - directed : bool, optional - If ``True``, the direction of the edge should be taken into account. - - Returns - ------- - bool - True if the edge exists. - False otherwise. - - See Also - -------- - :meth:`has_vertex`, :meth:`has_face`, :meth:`has_cell` - - """ - u, v = edge - if directed: - return u in self._edge and v in self._edge[u] - return (u in self._edge and v in self._edge[u]) or (v in self._edge and u in self._edge[v]) - - def edge_faces(self, edge): - """Return the faces adjacent to an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - - Returns - ------- - list[int] - The identifiers of the adjacent faces. - """ - u, v = edge - faces = set() - if v in self._plane[u]: - faces.update(self._plane[u][v].keys()) - if u in self._plane[v]: - faces.update(self._plane[v][u].keys()) - return sorted(list(faces)) - - def edge_cells(self, edge): - """Ordered cells around edge (u, v). - - Parameters - ---------- - edge : tuple[int, int] - The identifier of the edge. - - Returns - ------- - list[int] - Ordered list of keys identifying the ordered cells. - - See Also - -------- - :meth:`edge_halffaces` - - """ - # @Roman: should v, u also be checked? - u, v = edge - cells = [] - for cell in self._plane[u][v].values(): - if cell is not None: - cells.append(cell) - return cells - - # def is_edge_on_boundary(self, edge): - # """Verify that an edge is on the boundary. - - # Parameters - # ---------- - # edge : tuple[int, int] - # The identifier of the edge. - - # Returns - # ------- - # bool - # True if the edge is on the boundary. - # False otherwise. - - # See Also - # -------- - # :meth:`is_vertex_on_boundary`, :meth:`is_face_on_boundary`, :meth:`is_cell_on_boundary` - - # Notes - # ----- - # This method simply checks if u-v or v-u is on the edge of the cell network. - # The direction u-v does not matter. - - # """ - # u, v = edge - # return None in self._plane[u][v].values() - - def edges_without_face(self): - """Find the edges that are not part of a face. - - Returns - ------- - list[int] - The edges without face. - - """ - edges = {edge for edge in self.edges() if not self.edge_faces(edge)} - return list(edges) - - def nonmanifold_edges(self): - """Returns the edges that belong to more than two faces. - - Returns - ------- - list[int] - The edges without face. - - """ - edges = {edge for edge in self.edges() if len(self.edge_faces(edge)) > 2} - return list(edges) - - # -------------------------------------------------------------------------- - # Edge Geometry - # -------------------------------------------------------------------------- - - def edge_coordinates(self, edge, axes="xyz"): - """Return the coordinates of the start and end point of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - axes : str, optional - The axes along which the coordinates should be included. - - Returns - ------- - tuple[list[float], list[float]] - The coordinates of the start point. - The coordinates of the end point. - """ - u, v = edge - return self.vertex_coordinates(u, axes=axes), self.vertex_coordinates(v, axes=axes) - - def edge_start(self, edge): - """Return the start point of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - - Returns - ------- - :class:`compas.geometry.Point` - The start point. - """ - return self.vertex_point(edge[0]) - - def edge_end(self, edge): - """Return the end point of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - - Returns - ------- - :class:`compas.geometry.Point` - The end point. - """ - return self.vertex_point(edge[1]) - - def edge_midpoint(self, edge): - """Return the midpoint of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - - Returns - ------- - :class:`compas.geometry.Point` - The midpoint. - - See Also - -------- - :meth:`edge_start`, :meth:`edge_end`, :meth:`edge_point` - - """ - a, b = self.edge_coordinates(edge) - return Point(0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])) - - def edge_point(self, edge, t=0.5): - """Return the point at a parametric location along an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - t : float, optional - The location of the point on the edge. - If the value of `t` is outside the range 0-1, the point will - lie in the direction of the edge, but not on the edge vector. - - Returns - ------- - :class:`compas.geometry.Point` - The XYZ coordinates of the point. - - See Also - -------- - :meth:`edge_start`, :meth:`edge_end`, :meth:`edge_midpoint` - - """ - if t == 0: - return self.edge_start(edge) - if t == 1: - return self.edge_end(edge) - if t == 0.5: - return self.edge_midpoint(edge) - - a, b = self.edge_coordinates(edge) - ab = subtract_vectors(b, a) - return Point(*add_vectors(a, scale_vector(ab, t))) - - def edge_vector(self, edge): - """Return the vector of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - - Returns - ------- - :class:`compas.geometry.Vector` - The vector from start to end. - """ - a, b = self.edge_coordinates(edge) - return Vector.from_start_end(a, b) - - def edge_direction(self, edge): - """Return the direction vector of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - - Returns - ------- - :class:`compas.geometry.Vector` - The direction vector of the edge. - """ - return Vector(*normalize_vector(self.edge_vector(edge))) - - def edge_line(self, edge): - """Return the line representation of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - - Returns - ------- - :class:`compas.geometry.Line` - The line. - """ - return Line(*self.edge_coordinates(edge)) - - def edge_length(self, edge): - """Return the length of an edge. - - Parameters - ---------- - edge : tuple[int, int] - The edge identifier. - - Returns - ------- - float - The length of the edge. - """ - a, b = self.edge_coordinates(edge) - return distance_point_point(a, b) - - # -------------------------------------------------------------------------- - # Face Accessors - # -------------------------------------------------------------------------- - - def faces(self, data=False): - """Iterate over the halffaces of the cell network and yield faces. - - Parameters - ---------- - data : bool, optional - If True, yield the face attributes in addition to the face identifiers. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next face identifier. - If `data` is True, the next face as a (face, attr) tuple. - - See Also - -------- - :meth:`vertices`, :meth:`edges`, :meth:`cells` - - Notes - ----- - Volmesh faces have no topological meaning (analogous to an edge of a mesh). - They are typically used for geometric operations (i.e. planarisation). - Between the interface of two cells, there are two interior faces (one from each cell). - Only one of these two interior faces are returned as a "face". - The unique faces are found by comparing string versions of sorted vertex lists. - - """ - for face in self._face: - if not data: - yield face - else: - yield face, self.face_attributes(face) - - def faces_where(self, conditions=None, data=False, **kwargs): - """Get faces for which a certain condition or set of conditions is true. - - Parameters - ---------- - conditions : dict, optional - A set of conditions in the form of key-value pairs. - The keys should be attribute names. The values can be attribute - values or ranges of attribute values in the form of min/max pairs. - data : bool, optional - If True, yield the face attributes in addition to the identifiers. - **kwargs : dict[str, Any], optional - Additional conditions provided as named function arguments. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next face that matches the condition. - If `data` is True, the next face and its attributes. - - See Also - -------- - :meth:`faces_where_predicate` - :meth:`vertices_where`, :meth:`edges_where`, :meth:`cells_where` - - """ - conditions = conditions or {} - conditions.update(kwargs) - - for fkey in self.faces(): - is_match = True - - attr = self.face_attributes(fkey) or {} - - for name, value in conditions.items(): - method = getattr(self, name, None) - - if method and callable(method): - val = method(fkey) - elif name in attr: - val = attr[name] - else: - is_match = False - break - - if isinstance(val, list): - if value not in val: - is_match = False - break - elif isinstance(value, (tuple, list)): - minval, maxval = value - if val < minval or val > maxval: - is_match = False - break - else: - if value != val: - is_match = False - break - - if is_match: - if data: - yield fkey, attr - else: - yield fkey - - def faces_where_predicate(self, predicate, data=False): - """Get faces for which a certain condition or set of conditions is true using a lambda function. - - Parameters - ---------- - predicate : callable - The condition you want to evaluate. - The callable takes 2 parameters: the face identifier and the the face attributes, and should return True or False. - data : bool, optional - If True, yield the face attributes in addition to the identifiers. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next face that matches the condition. - If `data` is True, the next face and its attributes. - - See Also - -------- - :meth:`faces_where` - :meth:`vertices_where_predicate`, :meth:`edges_where_predicate`, :meth:`cells_where_predicate` - - """ - for fkey, attr in self.faces(True): - if predicate(fkey, attr): - if data: - yield fkey, attr - else: - yield fkey - - # -------------------------------------------------------------------------- - # Face Attributes - # -------------------------------------------------------------------------- - - def update_default_face_attributes(self, attr_dict=None, **kwattr): - """Update the default face attributes. - - Parameters - ---------- - attr_dict : dict[str, Any], optional - A dictionary of attributes with their default values. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - None - - See Also - -------- - :meth:`update_default_vertex_attributes`, :meth:`update_default_edge_attributes`, :meth:`update_default_cell_attributes` - - Notes - ----- - Named arguments overwrite correpsonding key-value pairs in the attribute dictionary. - - """ - if not attr_dict: - attr_dict = {} - attr_dict.update(kwattr) - self.default_face_attributes.update(attr_dict) - - def face_attribute(self, face, name, value=None): - """Get or set an attribute of a face. - - Parameters - ---------- - face : int - The face identifier. - name : str - The name of the attribute. - value : object, optional - The value of the attribute. - - Returns - ------- - object | None - The value of the attribute, or None when the function is used as a "setter". - - Raises - ------ - KeyError - If the face does not exist. - - See Also - -------- - :meth:`unset_face_attribute` - :meth:`face_attributes`, :meth:`faces_attribute`, :meth:`faces_attributes` - :meth:`vertex_attribute`, :meth:`edge_attribute`, :meth:`cell_attribute` - - """ - if face not in self._face: - raise KeyError(face) - - if value is not None: - if face not in self._face_data: - self._face_data[face] = {} - self._face_data[face][name] = value - return - if face in self._face_data and name in self._face_data[face]: - return self._face_data[face][name] - if name in self.default_face_attributes: - return self.default_face_attributes[name] - - def unset_face_attribute(self, face, name): - """Unset the attribute of a face. - - Parameters - ---------- - face : int - The face identifier. - name : str - The name of the attribute. - - Raises - ------ - KeyError - If the face does not exist. - - Returns - ------- - None - - See Also - -------- - :meth:`face_attribute` - - Notes - ----- - Unsetting the value of a face attribute implicitly sets it back to the value - stored in the default face attribute dict. - - """ - if face not in self._face: - raise KeyError(face) - - if face in self._face_data and name in self._face_data[face]: - del self._face_data[face][name] - - def face_attributes(self, face, names=None, values=None): - """Get or set multiple attributes of a face. - - Parameters - ---------- - face : int - The identifier of the face. - names : list[str], optional - A list of attribute names. - values : list[Any], optional - A list of attribute values. - - Returns - ------- - dict[str, Any] | list[Any] | None - If the parameter `names` is empty, a dictionary of all attribute name-value pairs of the face. - If the parameter `names` is not empty, a list of the values corresponding to the provided names. - None if the function is used as a "setter". - - Raises - ------ - KeyError - If the face does not exist. - - See Also - -------- - :meth:`face_attribute`, :meth:`faces_attribute`, :meth:`faces_attributes` - :meth:`vertex_attributes`, :meth:`edge_attributes`, :meth:`cell_attributes` - - """ - if face not in self._face: - raise KeyError(face) - - if names and values: - for name, value in zip(names, values): - if face not in self._face_data: - self._face_data[face] = {} - self._face_data[face][name] = value - return - - if not names: - return FaceAttributeView(self.default_face_attributes, self._face_data.setdefault(face, {})) - - values = [] - for name in names: - value = self.face_attribute(face, name) - values.append(value) - return values - - def faces_attribute(self, name, value=None, faces=None): - """Get or set an attribute of multiple faces. - - Parameters - ---------- - name : str - The name of the attribute. - value : object, optional - The value of the attribute. - Default is None. - faces : list[int], optional - A list of face identifiers. - - Returns - ------- - list[Any] | None - A list containing the value per face of the requested attribute, - or None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the faces does not exist. - - See Also - -------- - :meth:`face_attribute`, :meth:`face_attributes`, :meth:`faces_attributes` - :meth:`vertex_attribute`, :meth:`edge_attribute`, :meth:`cell_attribute` - - """ - faces = faces or self.faces() - if value is not None: - for face in faces: - self.face_attribute(face, name, value) - return - return [self.face_attribute(face, name) for face in faces] - - def faces_attributes(self, names=None, values=None, faces=None): - """Get or set multiple attributes of multiple faces. - - Parameters - ---------- - names : list[str], optional - The names of the attribute. - Default is None. - values : list[Any], optional - The values of the attributes. - Default is None. - faces : list[int], optional - A list of face identifiers. - - Returns - ------- - list[dict[str, Any]] | list[list[Any]] | None - If the parameter `names` is empty, - a list containing per face an attribute dict with all attributes (default + custom) of the face. - If the parameter `names` is not empty, - a list containing per face a list of attribute values corresponding to the requested names. - None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the faces does not exist. - - See Also - -------- - :meth:`face_attribute`, :meth:`face_attributes`, :meth:`faces_attribute` - :meth:`vertex_attributes`, :meth:`edge_attributes`, :meth:`cell_attributes` - - """ - faces = faces or self.faces() - if values: - for face in faces: - self.face_attributes(face, names, values) - return - return [self.face_attributes(face, names) for face in faces] - - # -------------------------------------------------------------------------- - # Face Topology - # -------------------------------------------------------------------------- - - def has_face(self, face): - """Verify that a face is part of the cell network. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - bool - True if the face exists. - False otherwise. - - See Also - -------- - :meth:`has_vertex`, :meth:`has_edge`, :meth:`has_cell` - - """ - return face in self._face - - def face_vertices(self, face): - """The vertices of a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - list[int] - Ordered vertex identifiers. - - """ - return self._face[face] - - def face_edges(self, face): - """The edges of a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - list[tuple[int, int]] - Ordered edge identifiers. - - """ - vertices = self.face_vertices(face) - edges = [] - for u, v in pairwise(vertices + vertices[:1]): - # if v in self._edge[u]: - # edges.append((u, v)) - edges.append((u, v)) - return edges - - def face_cells(self, face): - """Return the cells connected to a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - list[int] - The identifiers of the cells connected to the face. - - """ - u, v = self.face_vertices(face)[:2] - cells = [] - if v in self._plane[u]: - cell = self._plane[u][v][face] - if cell is not None: - cells.append(cell) - cell = self._plane[v][u][face] - if cell is not None: - cells.append(cell) - return cells - - def faces_without_cell(self): - """Find the faces that are not part of a cell. - - Returns - ------- - list[int] - The faces without cell. - - """ - faces = {fkey for fkey in self.faces() if not self.face_cells(fkey)} - 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. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - bool - True if the face is on the boundary. - False otherwise. - - """ - u, v = self.face_vertices(face)[:2] - cu = 1 if self._plane[u][v][face] is None else 0 - cv = 1 if self._plane[v][u][face] is None else 0 - return cu + cv == 1 - - def faces_on_boundaries(self): - """Find the faces that are on the boundary. - - Returns - ------- - list[int] - The faces on the boundary. - - """ - return [face for face in self.faces() if self.is_face_on_boundary(face)] - - # -------------------------------------------------------------------------- - # Face Geometry - # -------------------------------------------------------------------------- - - def face_coordinates(self, face, axes="xyz"): - """Compute the coordinates of the vertices of a face. - - Parameters - ---------- - face : int - The identifier of the face. - axes : str, optional - The axes alon which to take the coordinates. - Should be a combination of x, y, and z. - - Returns - ------- - list[list[float]] - The coordinates of the vertices of the face. - - See Also - -------- - :meth:`face_points`, :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_centroid`, :meth:`face_center` - :meth:`face_area`, :meth:`face_flatness`, :meth:`face_aspect_ratio` - - """ - return [self.vertex_coordinates(vertex, axes=axes) for vertex in self.face_vertices(face)] - - def face_points(self, face): - """Compute the points of the vertices of a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - list[:class:`compas.geometry.Point`] - The points of the vertices of the face. - - See Also - -------- - :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_centroid`, :meth:`face_center` - - """ - return [self.vertex_point(vertex) for vertex in self.face_vertices(face)] - - def face_polygon(self, face): - """Compute the polygon of a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - :class:`compas.geometry.Polygon` - The polygon of the face. - - See Also - -------- - :meth:`face_points`, :meth:`face_normal`, :meth:`face_centroid`, :meth:`face_center` - - """ - return Polygon(self.face_points(face)) - - def face_normal(self, face, unitized=True): - """Compute the oriented normal of a face. - - Parameters - ---------- - face : int - The identifier of the face. - unitized : bool, optional - If True, unitize the normal vector. - - Returns - ------- - :class:`compas.geometry.Vector` - The normal vector. - - See Also - -------- - :meth:`face_points`, :meth:`face_polygon`, :meth:`face_centroid`, :meth:`face_center` - - """ - return Vector(*normal_polygon(self.face_coordinates(face), unitized=unitized)) - - def face_centroid(self, face): - """Compute the point at the centroid of a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - :class:`compas.geometry.Point` - The coordinates of the centroid. - - See Also - -------- - :meth:`face_points`, :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_center` - - """ - return Point(*centroid_points(self.face_coordinates(face))) - - def face_center(self, face): - """Compute the point at the center of mass of a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - :class:`compas.geometry.Point` - The coordinates of the center of mass. - - See Also - -------- - :meth:`face_points`, :meth:`face_polygon`, :meth:`face_normal`, :meth:`face_centroid` - - """ - return Point(*centroid_polygon(self.face_coordinates(face))) - - def face_plane(self, face): - return Plane(self.face_center(face), self.face_normal(face)) - - def face_area(self, face): - """Compute the oriented area of a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - float - The non-oriented area of the face. - - See Also - -------- - :meth:`face_flatness`, :meth:`face_aspect_ratio` - - """ - return length_vector(self.face_normal(face, unitized=False)) - - def face_flatness(self, face, maxdev=0.02): - """Compute the flatness of a face. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - float - The flatness. - - See Also - -------- - :meth:`face_area`, :meth:`face_aspect_ratio` - - Notes - ----- - compas.geometry.mesh_flatness function currently only works for quadrilateral faces. - This function uses the distance between each face vertex and its projected point - on the best-fit plane of the face as the flatness metric. - - """ - deviation = 0 - polygon = self.face_coordinates(face) - plane = bestfit_plane(polygon) - for pt in polygon: - pt_proj = project_point_plane(pt, plane) - dev = distance_point_point(pt, pt_proj) - if dev > deviation: - deviation = dev - return deviation - - def face_aspect_ratio(self, face): - """Face aspect ratio as the ratio between the lengths of the maximum and minimum face edges. - - Parameters - ---------- - face : int - The identifier of the face. - - Returns - ------- - float - The aspect ratio. - - See Also - -------- - :meth:`face_area`, :meth:`face_flatness` - - References - ---------- - .. [1] Wikipedia. *Types of mesh*. - Available at: https://en.wikipedia.org/wiki/Types_of_mesh. - - """ - lengths = [self.edge_length(edge) for edge in self.face_edges(face)] - return max(lengths) / min(lengths) - - # -------------------------------------------------------------------------- - # Cell Accessors - # -------------------------------------------------------------------------- - - def cells(self, data=False): - """Iterate over the cells of the volmesh. - - Parameters - ---------- - data : bool, optional - If True, yield the cell attributes in addition to the cell identifiers. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next cell identifier. - If `data` is True, the next cell as a (cell, attr) tuple. - - See Also - -------- - :meth:`vertices`, :meth:`edges`, :meth:`faces` - - """ - for cell in self._cell: - if not data: - yield cell - else: - yield cell, self.cell_attributes(cell) - - def cells_where(self, conditions=None, data=False, **kwargs): - """Get cells for which a certain condition or set of conditions is true. - - Parameters - ---------- - conditions : dict, optional - A set of conditions in the form of key-value pairs. - The keys should be attribute names. The values can be attribute - values or ranges of attribute values in the form of min/max pairs. - data : bool, optional - If True, yield the cell attributes in addition to the identifiers. - **kwargs : dict[str, Any], optional - Additional conditions provided as named function arguments. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next cell that matches the condition. - If `data` is True, the next cell and its attributes. - - See Also - -------- - :meth:`cells_where_predicate` - :meth:`vertices_where`, :meth:`edges_where`, :meth:`faces_where` - - """ - conditions = conditions or {} - conditions.update(kwargs) - - for ckey in self.cells(): - is_match = True - - attr = self.cell_attributes(ckey) or {} - - for name, value in conditions.items(): - method = getattr(self, name, None) - - if method and callable(method): - val = method(ckey) - elif name in attr: - val = attr[name] - else: - is_match = False - break - - if isinstance(val, list): - if value not in val: - is_match = False - break - elif isinstance(value, (tuple, list)): - minval, maxval = value - if val < minval or val > maxval: - is_match = False - break - else: - if value != val: - is_match = False - break - - if is_match: - if data: - yield ckey, attr - else: - yield ckey - - def cells_where_predicate(self, predicate, data=False): - """Get cells for which a certain condition or set of conditions is true using a lambda function. - - Parameters - ---------- - predicate : callable - The condition you want to evaluate. - The callable takes 2 parameters: the cell identifier and the cell attributes, and should return True or False. - data : bool, optional - If True, yield the cell attributes in addition to the identifiers. - - Yields - ------ - int | tuple[int, dict[str, Any]] - If `data` is False, the next cell that matches the condition. - If `data` is True, the next cell and its attributes. - - See Also - -------- - :meth:`cells_where` - :meth:`vertices_where_predicate`, :meth:`edges_where_predicate`, :meth:`faces_where_predicate` - - """ - for ckey, attr in self.cells(True): - if predicate(ckey, attr): - if data: - yield ckey, attr - else: - yield ckey - - # -------------------------------------------------------------------------- - # Cell Attributes - # -------------------------------------------------------------------------- - - def update_default_cell_attributes(self, attr_dict=None, **kwattr): - """Update the default cell attributes. - - Parameters - ---------- - attr_dict : dict[str, Any], optional - A dictionary of attributes with their default values. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - None - - See Also - -------- - :meth:`update_default_vertex_attributes`, :meth:`update_default_edge_attributes`, :meth:`update_default_face_attributes` - - Notes - ----- - Named arguments overwrite corresponding cell-value pairs in the attribute dictionary. - - """ - if not attr_dict: - attr_dict = {} - attr_dict.update(kwattr) - self.default_cell_attributes.update(attr_dict) - - def cell_attribute(self, cell, name, value=None): - """Get or set an attribute of a cell. - - Parameters - ---------- - cell : int - The cell identifier. - name : str - The name of the attribute. - value : object, optional - The value of the attribute. - - Returns - ------- - object | None - The value of the attribute, or None when the function is used as a "setter". - - Raises - ------ - KeyError - If the cell does not exist. - - See Also - -------- - :meth:`unset_cell_attribute` - :meth:`cell_attributes`, :meth:`cells_attribute`, :meth:`cells_attributes` - :meth:`vertex_attribute`, :meth:`edge_attribute`, :meth:`face_attribute` - - """ - if cell not in self._cell: - raise KeyError(cell) - if value is not None: - if cell not in self._cell_data: - self._cell_data[cell] = {} - self._cell_data[cell][name] = value - return - if cell in self._cell_data and name in self._cell_data[cell]: - return self._cell_data[cell][name] - if name in self.default_cell_attributes: - return self.default_cell_attributes[name] - - def unset_cell_attribute(self, cell, name): - """Unset the attribute of a cell. - - Parameters - ---------- - cell : int - The cell identifier. - name : str - The name of the attribute. - - Returns - ------- - None - - Raises - ------ - KeyError - If the cell does not exist. - - See Also - -------- - :meth:`cell_attribute` - - Notes - ----- - Unsetting the value of a cell attribute implicitly sets it back to the value - stored in the default cell attribute dict. - - """ - if cell not in self._cell: - raise KeyError(cell) - if cell in self._cell_data: - if name in self._cell_data[cell]: - del self._cell_data[cell][name] - - def cell_attributes(self, cell, names=None, values=None): - """Get or set multiple attributes of a cell. - - Parameters - ---------- - cell : int - The identifier of the cell. - names : list[str], optional - A list of attribute names. - values : list[Any], optional - A list of attribute values. - - Returns - ------- - dict[str, Any] | list[Any] | None - If the parameter `names` is empty, a dictionary of all attribute name-value pairs of the cell. - If the parameter `names` is not empty, a list of the values corresponding to the provided names. - None if the function is used as a "setter". - - Raises - ------ - KeyError - If the cell does not exist. - - See Also - -------- - :meth:`cell_attribute`, :meth:`cells_attribute`, :meth:`cells_attributes` - :meth:`vertex_attributes`, :meth:`edge_attributes`, :meth:`face_attributes` - - """ - if cell not in self._cell: - raise KeyError(cell) - if names and values is not None: - for name, value in zip(names, values): - if cell not in self._cell_data: - self._cell_data[cell] = {} - self._cell_data[cell][name] = value - return - if not names: - return CellAttributeView(self.default_cell_attributes, self._cell_data.setdefault(cell, {})) - values = [] - for name in names: - value = self.cell_attribute(cell, name) - values.append(value) - return values - - def cells_attribute(self, name, value=None, cells=None): - """Get or set an attribute of multiple cells. - - Parameters - ---------- - name : str - The name of the attribute. - value : object, optional - The value of the attribute. - cells : list[int], optional - A list of cell identifiers. - - Returns - ------- - list[Any] | None - A list containing the value per face of the requested attribute, - or None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the cells does not exist. - - See Also - -------- - :meth:`cell_attribute`, :meth:`cell_attributes`, :meth:`cells_attributes` - :meth:`vertex_attribute`, :meth:`edge_attribute`, :meth:`face_attribute` - - """ - if not cells: - cells = self.cells() - if value is not None: - for cell in cells: - self.cell_attribute(cell, name, value) - return - return [self.cell_attribute(cell, name) for cell in cells] - - def cells_attributes(self, names=None, values=None, cells=None): - """Get or set multiple attributes of multiple cells. - - Parameters - ---------- - names : list[str], optional - The names of the attribute. - Default is None. - values : list[Any], optional - The values of the attributes. - Default is None. - cells : list[int], optional - A list of cell identifiers. - - Returns - ------- - list[dict[str, Any]] | list[list[Any]] | None - If the parameter `names` is empty, - a list containing per cell an attribute dict with all attributes (default + custom) of the cell. - If the parameter `names` is empty, - a list containing per cell a list of attribute values corresponding to the requested names. - None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the faces does not exist. - - See Also - -------- - :meth:`cell_attribute`, :meth:`cell_attributes`, :meth:`cells_attribute` - :meth:`vertex_attributes`, :meth:`edge_attributes`, :meth:`face_attributes` - - """ - if not cells: - cells = self.cells() - if values is not None: - for cell in cells: - self.cell_attributes(cell, names, values) - return - return [self.cell_attributes(cell, names) for cell in cells] - - # -------------------------------------------------------------------------- - # Cell Topology - # -------------------------------------------------------------------------- - - def cell_vertices(self, cell): - """The vertices of a cell. - - Parameters - ---------- - cell : int - Identifier of the cell. - - Returns - ------- - list[int] - The vertex identifiers of a cell. - - See Also - -------- - :meth:`cell_edges`, :meth:`cell_faces`, :meth:`cell_halfedges` - - Notes - ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.vertices`, - but in the context of a cell of the `VolMesh`. - - """ - return list(set([vertex for face in self.cell_faces(cell) for vertex in self.face_vertices(face)])) - - def cell_halfedges(self, cell): - """The halfedges of a cell. - - Parameters - ---------- - cell : int - Identifier of the cell. - - Returns - ------- - list[tuple[int, int]] - The halfedges of a cell. - - See Also - -------- - :meth:`cell_edges`, :meth:`cell_faces`, :meth:`cell_vertices` - - Notes - ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.halfedges`, - but in the context of a cell of the `VolMesh`. - - """ - halfedges = [] - for u in self._cell[cell]: - for v in self._cell[cell][u]: - halfedges.append((u, v)) - return halfedges - - def cell_edges(self, cell): - """Return all edges of a cell. - - Parameters - ---------- - cell : int - The cell identifier. - - Returns - ------- - list[tuple[int, int]] - The edges of the cell. - - See Also - -------- - :meth:`cell_halfedges`, :meth:`cell_faces`, :meth:`cell_vertices` - - Notes - ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.edges`, - but in the context of a cell of the `VolMesh`. - - """ - return self.cell_halfedges(cell) - - def cell_faces(self, cell): - """The faces of a cell. - - Parameters - ---------- - cell : int - Identifier of the cell. - - Returns - ------- - list[int] - The faces of a cell. - - See Also - -------- - :meth:`cell_halfedges`, :meth:`cell_edges`, :meth:`cell_vertices` - - Notes - ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.faces`, - but in the context of a cell of the `VolMesh`. - - """ - faces = set() - for vertex in self._cell[cell]: - faces.update(self._cell[cell][vertex].values()) - return list(faces) - - def cell_vertex_neighbors(self, cell, vertex): - """Ordered vertex neighbors of a vertex of a cell. - - Parameters - ---------- - cell : int - Identifier of the cell. - vertex : int - Identifier of the vertex. - - Returns - ------- - list[int] - The list of neighboring vertices. - - See Also - -------- - :meth:`cell_vertex_faces` - - Notes - ----- - All of the returned vertices are part of the cell. - - This method is similar to :meth:`~compas.datastructures.HalfEdge.vertex_neighbors`, - but in the context of a cell of the `VolMesh`. - - """ - if vertex not in self._cell[cell]: - raise KeyError(vertex) - - nbrs = [] - for nbr in self._vertex[vertex]: - if nbr in self._cell[cell]: - nbrs.append(nbr) - - return nbrs - - # nbr_vertices = self._cell[cell][vertex].keys() - # v = nbr_vertices[0] - # ordered_vkeys = [v] - # for i in range(len(nbr_vertices) - 1): - # face = self._cell[cell][vertex][v] - # v = self.halfface_vertex_ancestor(face, vertex) - # ordered_vkeys.append(v) - # return ordered_vkeys - - def cell_vertex_faces(self, cell, vertex): - """Ordered faces connected to a vertex of a cell. - - Parameters - ---------- - cell : int - Identifier of the cell. - vertex : int - Identifier of the vertex. - - Returns - ------- - list[int] - The ordered list of faces connected to a vertex of a cell. - - See Also - -------- - :meth:`cell_vertex_neighbors` - - Notes - ----- - All of the returned faces should are part of the same cell. - - This method is similar to :meth:`~compas.datastructures.HalfEdge.vertex_faces`, - but in the context of a cell of the `VolMesh`. - - """ - # nbr_vertices = self._cell[cell][vertex].keys() - # u = vertex - # v = nbr_vertices[0] - # ordered_faces = [] - # for i in range(len(nbr_vertices)): - # face = self._cell[cell][u][v] - # v = self.halfface_vertex_ancestor(face, u) - # ordered_faces.append(face) - # return ordered_faces - - if vertex not in self._cell[cell]: - raise KeyError(vertex) - - faces = [] - for nbr in self._cell[cell][vertex]: - faces.append(self._cell[cell][vertex][nbr]) - - return faces - - def cell_face_vertices(self, cell, face): - """The vertices of a face of a cell. - - Parameters - ---------- - cell : int - Identifier of the cell. - face : int - Identifier of the face. - - Returns - ------- - list[int] - The vertices of the face of the cell. - - See Also - -------- - :meth:`cell_face_halfedges` - - Notes - ----- - All of the returned vertices are part of the cell. - - This method is similar to :meth:`~compas.datastructures.HalfEdge.face_vertices`, - but in the context of a cell of the `VolMesh`. - - """ - if face not in self._face: - raise KeyError(face) - - vertices = self.face_vertices(face) - u, v = vertices[:2] - if v in self._cell[cell][u] and self._cell[cell][u][v] == face: - return self.face_vertices(face) - if u in self._cell[cell][v] and self._cell[cell][v][u] == face: - return self.face_vertices(face)[::-1] - - raise Exception("Face is not part of the cell") - - def cell_face_halfedges(self, cell, face): - """The halfedges of a face of a cell. - - Parameters - ---------- - cell : int - Identifier of the cell. - face : int - Identifier of the face. - - Returns - ------- - list[tuple[int, int]] - The halfedges of the face of the cell. - - See Also - -------- - :meth:`cell_face_vertices` - - Notes - ----- - All of the returned halfedges are part of the cell. - - This method is similar to :meth:`~compas.datastructures.HalfEdge.face_halfedges`, - but in the context of a cell of the `VolMesh`. - - """ - vertices = self.cell_face_vertices(cell, face) - return list(pairwise(vertices + vertices[:1])) - - def cell_halfedge_face(self, cell, halfedge): - """Find the face corresponding to a specific halfedge of a cell. - - Parameters - ---------- - cell : int - The identifier of the cell. - halfedge : tuple[int, int] - The identifier of the halfedge. - - Returns - ------- - int - The identifier of the face. - - See Also - -------- - :meth:`cell_halfedge_opposite_face` - - Notes - ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.halfedge_face`, - but in the context of a cell of the `VolMesh`. - - """ - u, v = halfedge - if u not in self._cell[cell] or v not in self._cell[cell][u]: - raise KeyError(halfedge) - return self._cell[cell][u][v] - - # def cell_halfedge_opposite_face(self, cell, halfedge): - # """Find the opposite face corresponding to a specific halfedge of a cell. - - # Parameters - # ---------- - # cell : int - # The identifier of the cell. - # halfedge : tuple[int, int] - # The identifier of the halfedge. - - # Returns - # ------- - # int - # The identifier of the face. - - # See Also - # -------- - # :meth:`cell_halfedge_face` - - # """ - # u, v = halfedge - # return self._cell[cell][v][u] - - def cell_face_neighbors(self, cell, face): - """Find the faces adjacent to a given face of a cell. - - Parameters - ---------- - cell : int - The identifier of the cell. - face : int - The identifier of the face. - - Returns - ------- - int - The identifier of the face. - - See Also - -------- - :meth:`cell_neighbors` - - Notes - ----- - This method is similar to :meth:`~compas.datastructures.HalfEdge.face_neighbors`, - but in the context of a cell of the `VolMesh`. - - """ - # nbrs = [] - # for halfedge in self.halfface_halfedges(face): - # nbr = self.cell_halfedge_opposite_face(cell, halfedge) - # if nbr is not None: - # nbrs.append(nbr) - # return nbrs - - nbrs = [] - for u in self.face_vertices(face): - for v in self._cell[cell][u]: - test = self._cell[cell][u][v] - if test == face: - nbr = self._cell[cell][v][u] - if nbr is not None: - 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. - - # Returns - # ------- - # list[int] - # The identifiers of the adjacent cells. - - # See Also - # -------- - # :meth:`cell_face_neighbors` - - # """ - # nbrs = [] - # for face in self.cell_faces(cell): - # nbr = self.halfface_opposite_cell(face) - # if nbr is not None: - # nbrs.append(nbr) - # return nbrs - - def is_cell_on_boundary(self, cell): - """Verify that a cell is on the boundary. - - Parameters - ---------- - cell : int - Identifier of the cell. - - Returns - ------- - bool - True if the face is on the boundary. - False otherwise. - - See Also - -------- - :meth:`is_vertex_on_boundary`, :meth:`is_edge_on_boundary`, :meth:`is_face_on_boundary` - - """ - faces = self.cell_faces(cell) - for face in faces: - if self.is_face_on_boundary(face): - return True - return False - - def cells_on_boundaries(self): - """Find the cells on the boundary. - - Returns - ------- - list[int] - The cells of the boundary. - - See Also - -------- - :meth:`vertices_on_boundaries`, :meth:`faces_on_boundaries` - - """ - cells = [] - for cell in self.cells(): - if self.is_cell_on_boundary(cell): - cells.append(cell) - return cells - - # -------------------------------------------------------------------------- - # Cell Geometry - # -------------------------------------------------------------------------- - - def cell_points(self, cell): - """Compute the points of the vertices of a cell. - - Parameters - ---------- - cell : int - The identifier of the cell. - - Returns - ------- - list[:class:`compas.geometry.Point`] - The points of the vertices of the cell. - - See Also - -------- - :meth:`cell_polygon`, :meth:`cell_centroid`, :meth:`cell_center` - """ - return [self.vertex_point(vertex) for vertex in self.cell_vertices(cell)] - - def cell_centroid(self, cell): - """Compute the point at the centroid of a cell. - - Parameters - ---------- - cell : int - The identifier of the cell. - - Returns - ------- - :class:`compas.geometry.Point` - The coordinates of the centroid. - - See Also - -------- - :meth:`cell_center` - """ - vertices = self.cell_vertices(cell) - return Point(*centroid_points([self.vertex_coordinates(vertex) for vertex in vertices])) - - def cell_center(self, cell): - """Compute the point at the center of mass of a cell. - - Parameters - ---------- - cell : int - The identifier of the cell. - - Returns - ------- - :class:`compas.geometry.Point` - The coordinates of the center of mass. - - See Also - -------- - :meth:`cell_centroid` - """ - vertices, faces = self.cell_to_vertices_and_faces(cell) - return Point(*centroid_polyhedron((vertices, faces))) - - # def cell_vertex_normal(self, cell, vertex): - # """Return the normal vector at the vertex of a boundary cell as the weighted average of the - # normals of the neighboring faces. - - # Parameters - # ---------- - # cell : int - # The identifier of the vertex of the cell. - # vertex : int - # The identifier of the vertex of the cell. - - # Returns - # ------- - # :class:`compas.geometry.Vector` - # The components of the normal vector. - - # """ - # cell_faces = self.cell_faces(cell) - # vectors = [self.face_normal(face) for face in self.vertex_halffaces(vertex) if face in cell_faces] - # return Vector(*normalize_vector(centroid_points(vectors))) - - def cell_polyhedron(self, cell): - """Construct a polyhedron from the vertices and faces of a cell. - - Parameters - ---------- - cell : int - The identifier of the cell. - - Returns - ------- - :class:`compas.geometry.Polyhedron` - The polyhedron. - - """ - vertices, faces = self.cell_to_vertices_and_faces(cell) - return Polyhedron(vertices, faces) - - # # -------------------------------------------------------------------------- - # # Boundaries - # # -------------------------------------------------------------------------- - - # def vertices_on_boundaries(self): - # """Find the vertices on the boundary. - - # Returns - # ------- - # list[int] - # The vertices of the boundary. - - # See Also - # -------- - # :meth:`faces_on_boundaries`, :meth:`cells_on_boundaries` - - # """ - # vertices = set() - # for face in self._halfface: - # if self.is_halfface_on_boundary(face): - # vertices.update(self.halfface_vertices(face)) - # return list(vertices) - - # def halffaces_on_boundaries(self): - # """Find the faces on the boundary. - - # Returns - # ------- - # list[int] - # The faces of the boundary. - - # See Also - # -------- - # :meth:`vertices_on_boundaries`, :meth:`cells_on_boundaries` - - # """ - # faces = set() - # for face in self._halfface: - # if self.is_halfface_on_boundary(face): - # faces.add(face) - # return list(faces) - - # def cells_on_boundaries(self): - # """Find the cells on the boundary. - - # Returns - # ------- - # list[int] - # The cells of the boundary. - - # See Also - # -------- - # :meth:`vertices_on_boundaries`, :meth:`faces_on_boundaries` - - # """ - # cells = set() - # for face in self.halffaces_on_boundaries(): - # cells.add(self.halfface_cell(face)) - # return list(cells) - - # # -------------------------------------------------------------------------- - # # Transformations - # # -------------------------------------------------------------------------- - - # def transform(self, T): - # """Transform the mesh. - - # Parameters - # ---------- - # T : :class:`Transformation` - # The transformation used to transform the mesh. - - # Returns - # ------- - # None - # The mesh is modified in-place. - - # Examples - # -------- - # >>> from compas.datastructures import Mesh - # >>> from compas.geometry import matrix_from_axis_and_angle - # >>> mesh = Mesh.from_polyhedron(6) - # >>> T = matrix_from_axis_and_angle([0, 0, 1], math.pi / 4) - # >>> mesh.transform(T) - - # """ - # points = transform_points(self.vertices_attributes("xyz"), T) - # for vertex, point in zip(self.vertices(), points): - # self.vertex_attributes(vertex, "xyz", point) From dea35a1a309a9e16b4c63db49b3fad51604e775c Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Sun, 26 May 2024 16:49:24 +0200 Subject: [PATCH 13/21] Delete cellnetworkobject.py --- src/compas/scene/cellnetworkobject.py | 252 -------------------------- 1 file changed, 252 deletions(-) delete mode 100644 src/compas/scene/cellnetworkobject.py diff --git a/src/compas/scene/cellnetworkobject.py b/src/compas/scene/cellnetworkobject.py deleted file mode 100644 index 8739516fe56..00000000000 --- a/src/compas/scene/cellnetworkobject.py +++ /dev/null @@ -1,252 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from compas.geometry import transform_points - -from .descriptors.colordict import ColorDictAttribute -from .sceneobject import SceneObject - - -class CellNetworkObject(SceneObject): - """Scene object for drawing volmesh data structures. - - Parameters - ---------- - volmesh : :class:`compas.datastructures.VolMesh` - A COMPAS volmesh. - - Attributes - ---------- - volmesh : :class:`compas.datastructures.VolMesh` - The COMPAS volmesh associated with the scene object. - vertex_xyz : dict[int, list[float]] - The view coordinates of the vertices. - By default, the actual vertex coordinates are used. - vertexcolor : :class:`compas.colors.ColorDict` - Mapping between vertices and colors. - Missing vertices get the default vertex color: :attr:`default_vertexcolor`. - edgecolor : :class:`compas.colors.ColorDict` - Mapping between edges and colors. - Missing edges get the default edge color: :attr:`default_edgecolor`. - facecolor : :class:`compas.colors.ColorDict` - Mapping between faces and colors. - Missing faces get the default face color: :attr:`default_facecolor`. - cellcolor : :class:`compas.colors.ColorDict` - Mapping between cells and colors. - Missing cells get the default cell color: :attr:`default_facecolor`. - vertexsize : float - 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 ``False``. - show_cells : bool - Flag for showing or hiding the cells, or a list of keys for the cells to show. - Default is ``True``. - - See Also - -------- - :class:`compas.scene.GraphObject` - :class:`compas.scene.MeshObject` - - """ - - vertexcolor = ColorDictAttribute() - edgecolor = ColorDictAttribute() - facecolor = ColorDictAttribute() - cellcolor = ColorDictAttribute() - - def __init__(self, volmesh, **kwargs): - super(CellNetworkObject, self).__init__(item=volmesh, **kwargs) - self._volmesh = None - 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) - - @property - def volmesh(self): - return self._volmesh - - @volmesh.setter - def volmesh(self, volmesh): - self._volmesh = volmesh - self._transformation = None - self._vertex_xyz = None - - @property - def transformation(self): - return self._transformation - - @transformation.setter - def transformation(self, transformation): - self._vertex_xyz = None - self._transformation = transformation - - @property - def vertex_xyz(self): - 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 - - @vertex_xyz.setter - def vertex_xyz(self, vertex_xyz): - self._vertex_xyz = vertex_xyz - - def draw_vertices(self, vertices=None, color=None, text=None): - """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. - - Returns - ------- - list - The identifiers of the objects representing the vertices in the visualization context. - - """ - raise NotImplementedError - - def draw_edges(self, edges=None, color=None, text=None): - """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. - - Returns - ------- - list - The identifiers of the objects representing the edges in the visualization context. - - """ - raise NotImplementedError - - def draw_faces(self, faces=None, color=None, text=None): - """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. - - Returns - ------- - list - The identifiers of the objects representing the faces in the visualization context. - - """ - raise NotImplementedError - - def draw_cells(self, cells=None, color=None, text=None): - """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. - - Returns - ------- - list - The identifiers of the objects representing the cells in the visualization context. - - """ - raise NotImplementedError - - def draw(self): - """Draw the volmesh.""" - raise NotImplementedError - - def clear_vertices(self): - """Clear the vertices of the mesh. - - Returns - ------- - None - - """ - raise NotImplementedError - - def clear_edges(self): - """Clear the edges of the mesh. - - Returns - ------- - None - - """ - raise NotImplementedError - - def clear_faces(self): - """Clear the faces of the mesh. - - Returns - ------- - None - - """ - raise NotImplementedError - - def clear_cells(self): - """Clear the cells of the mesh. - - Returns - ------- - None - - """ - raise NotImplementedError From 9f9c4097e5ccd8f405e8690055f4a6ae6d93839c Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Sun, 26 May 2024 16:58:34 +0200 Subject: [PATCH 14/21] Update cell_network.py --- .../cell_network/cell_network.py | 96 +------------------ 1 file changed, 2 insertions(+), 94 deletions(-) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 47431dbe386..9efe9513b88 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -183,7 +183,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()}, + "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, @@ -209,7 +209,7 @@ def __from_data__(cls, data): for key, attr in iter(vertex.items()): 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()} + edge_data = {literal_eval(k): v for k, v in data.get("edge_data", {}).items()} for u in edge: for v in edge[u]: attr = edge_data.get(tuple(sorted((int(u), int(v)))), {}) @@ -264,98 +264,6 @@ def __str__(self): self.number_of_edges(), ) - # -------------------------------------------------------------------------- - # Data - # -------------------------------------------------------------------------- - - @property - def data(self): - """Returns a dictionary of structured data representing the cell network data object. - - Note that some of the data stored internally in the data structure object is not included in the dictionary representation of the object. - This is the case for data that is considered private and/or redundant. - Specifically, the plane dictionary are not included. - This is because the information in these dictionaries can be reconstructed from the other data. - Therefore, to keep the dictionary representation as compact as possible, these dictionaries are not included. - - Returns - ------- - dict - The structured data representing the cell network. - - """ - cell = {} - for c in self._cell: - faces = set() - for u in self._cell[c]: - for v in self._cell[c][u]: - faces.add(self._cell[c][u][v]) - cell[c] = sorted(list(faces)) - - return { - "attributes": self.attributes, - "dva": self.default_vertex_attributes, - "dea": self.default_edge_attributes, - "dfa": self.default_face_attributes, - "dca": self.default_cell_attributes, - "vertex": self._vertex, - "edge": self._edge, - "face": self._face, - "cell": cell, - "edge_data": self._edge_data, - "face_data": self._face_data, - "cell_data": self._cell_data, - "max_vertex": self._max_vertex, - "max_face": self._max_face, - "max_cell": self._max_cell, - } - - @classmethod - def from_data(cls, data): - dva = data.get("dva") or {} - dea = data.get("dea") or {} - dfa = data.get("dfa") or {} - dca = data.get("dca") or {} - - cell_network = cls( - default_vertex_attributes=dva, - default_edge_attributes=dea, - default_face_attributes=dfa, - default_cell_attributes=dca, - ) - - cell_network.attributes.update(data.get("attributes") or {}) - - vertex = data.get("vertex") or {} - edge = data.get("edge") or {} - face = data.get("face") or {} - cell = data.get("cell") or {} - - for key, attr in iter(vertex.items()): - cell_network.add_vertex(key=key, attr_dict=attr) - - edge_data = data.get("edge_data") or {} - for u in edge: - for v in edge[u]: - attr = edge_data.get(tuple(sorted((u, v))), {}) - cell_network.add_edge(u, v, attr_dict=attr) - - face_data = data.get("face_data") or {} - for key, vertices in iter(face.items()): - attr = face_data.get(key) or {} - cell_network.add_face(vertices, fkey=key, attr_dict=attr) - - cell_data = data.get("cell_data") or {} - for ckey, faces in iter(cell.items()): - attr = cell_data.get(ckey) or {} - cell_network.add_cell(faces, ckey=ckey, attr_dict=attr) - - cell_network._max_vertex = data.get("max_vertex", cell_network._max_vertex) - cell_network._max_face = data.get("max_face", cell_network._max_face) - cell_network._max_cell = data.get("max_cell", cell_network._max_cell) - - return cell_network - # -------------------------------------------------------------------------- # Helpers # -------------------------------------------------------------------------- From 5583ea4c64f78282d8bc59cd3d3aba9660acb2a4 Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Sun, 26 May 2024 16:58:34 +0200 Subject: [PATCH 15/21] Update cell_network.py --- .../cell_network/cell_network.py | 94 +------------------ 1 file changed, 2 insertions(+), 92 deletions(-) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 6d56ebdea48..920199e12b5 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -184,6 +184,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, @@ -209,6 +210,7 @@ def __from_data__(cls, data): for key, attr in iter(vertex.items()): cell_network.add_vertex(key=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) @@ -270,98 +272,6 @@ def __str__(self): self.number_of_edges(), ) - # -------------------------------------------------------------------------- - # Data - # -------------------------------------------------------------------------- - - @property - def data(self): - """Returns a dictionary of structured data representing the cell network data object. - - Note that some of the data stored internally in the data structure object is not included in the dictionary representation of the object. - This is the case for data that is considered private and/or redundant. - Specifically, the plane dictionary are not included. - This is because the information in these dictionaries can be reconstructed from the other data. - Therefore, to keep the dictionary representation as compact as possible, these dictionaries are not included. - - Returns - ------- - dict - The structured data representing the cell network. - - """ - cell = {} - for c in self._cell: - faces = set() - for u in self._cell[c]: - for v in self._cell[c][u]: - faces.add(self._cell[c][u][v]) - cell[c] = sorted(list(faces)) - - return { - "attributes": self.attributes, - "dva": self.default_vertex_attributes, - "dea": self.default_edge_attributes, - "dfa": self.default_face_attributes, - "dca": self.default_cell_attributes, - "vertex": self._vertex, - "edge": self._edge, - "face": self._face, - "cell": cell, - "edge_data": self._edge_data, - "face_data": self._face_data, - "cell_data": self._cell_data, - "max_vertex": self._max_vertex, - "max_face": self._max_face, - "max_cell": self._max_cell, - } - - @classmethod - def from_data(cls, data): - dva = data.get("dva") or {} - dea = data.get("dea") or {} - dfa = data.get("dfa") or {} - dca = data.get("dca") or {} - - cell_network = cls( - default_vertex_attributes=dva, - default_edge_attributes=dea, - default_face_attributes=dfa, - default_cell_attributes=dca, - ) - - cell_network.attributes.update(data.get("attributes") or {}) - - vertex = data.get("vertex") or {} - edge = data.get("edge") or {} - face = data.get("face") or {} - cell = data.get("cell") or {} - - for key, attr in iter(vertex.items()): - cell_network.add_vertex(key=key, attr_dict=attr) - - edge_data = data.get("edge_data") or {} - for u in edge: - for v in edge[u]: - attr = edge_data.get(tuple(sorted((u, v))), {}) - cell_network.add_edge(u, v, attr_dict=attr) - - face_data = data.get("face_data") or {} - for key, vertices in iter(face.items()): - attr = face_data.get(key) or {} - cell_network.add_face(vertices, fkey=key, attr_dict=attr) - - cell_data = data.get("cell_data") or {} - for ckey, faces in iter(cell.items()): - attr = cell_data.get(ckey) or {} - cell_network.add_cell(faces, ckey=ckey, attr_dict=attr) - - cell_network._max_vertex = data.get("max_vertex", cell_network._max_vertex) - cell_network._max_face = data.get("max_face", cell_network._max_face) - cell_network._max_cell = data.get("max_cell", cell_network._max_cell) - - return cell_network - # -------------------------------------------------------------------------- # Helpers # -------------------------------------------------------------------------- From 1109a0d59220f8be9a3c39f78169cf92be9fb397 Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Sun, 26 May 2024 18:40:19 +0200 Subject: [PATCH 16/21] updates --- ...tastructures.cellnetworks.example_hull.png | Bin 0 -> 32386 bytes .../basics.datastructures.cellnetwork.rst | 65 ++++++++++++- .../basics.datastructures.cellnetworks.py | 27 ++---- docs/userguide/basics.datastructures.rst | 1 + .../cell_network/cell_network.py | 88 +++++++++--------- 5 files changed, 113 insertions(+), 68 deletions(-) create mode 100644 docs/_images/userguide/basics.datastructures.cellnetworks.example_hull.png 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 0000000000000000000000000000000000000000..29f355ecc985a6fc1b482cd8f587033986eff185 GIT binary patch literal 32386 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 import compas >>> from compas.datastructures import CellNetwork >>> from compas.scene import Scene >>> cell_network = CellNetwork.from_json(compas.get('cellnetwork_example.json')) @@ -69,11 +70,65 @@ For more information about visualisation with :class:`compas.scene.Scene`, see : .. 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. -A face can be at maximum assigned to two cells, to one or None. -Faces have typically edges as boundaries, but an edge can also exist without being part of a face. + * 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. + +>>> cell_network.edge_faces((2, 6)) +[2, 3, 39] +>>> cell_network.edge_faces((1, 10)) +[8] +>>> cell_network.edges_without_face() +[(43, 34)] + +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.do_faces_form_a_closed_cell(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 belonging to no face, shown with thicker linewidth. +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 index 45cf3b12e25..a4729ae41b2 100644 --- a/docs/userguide/basics.datastructures.cellnetworks.py +++ b/docs/userguide/basics.datastructures.cellnetworks.py @@ -4,37 +4,22 @@ import compas from compas.colors import Color +from compas.datastructures import CellNetwork from compas.geometry import Line, Polygon -HERE = os.path.dirname(__file__) -cell_network = compas.json_load(os.path.join(HERE, "basics.datastructures.cell_networks.json")) - -""" -viewer = Viewer() -for face in cell_network.faces(): - viewer.scene.add(Polygon(cell_network.face_coordinates(face)), facecolor=Color.silver()) -for edge in cell_network.edges_without_face(): - line = Line(*cell_network.edge_coordinates(edge)) - viewer.scene.add(line, linewidth=3) -viewer.show() -""" - +cell_network = CellNetwork.from_json(compas.get("cellnetwork_example.json")) 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 = Color.silver() - opacity = 0.5 + color, opacity = Color.silver(), 0.5 elif face in no_cell: - color = Color.azure() - opacity = 0.3 + color, opacity = Color.azure(), 0.3 else: - color = Color.yellow() - opacity = 0.8 + 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) -viewer.show() - +viewer.show() \ No newline at end of file diff --git a/docs/userguide/basics.datastructures.rst b/docs/userguide/basics.datastructures.rst index dddfd52f3eb..51e55c090f4 100644 --- a/docs/userguide/basics.datastructures.rst +++ b/docs/userguide/basics.datastructures.rst @@ -10,5 +10,6 @@ Datastructures basics.datastructures.graphs basics.datastructures.meshes basics.datastructures.cells + basics.datastructures.cellnetwork basics.datastructures.trees basics.datastructures.assemblies diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 9efe9513b88..8aa1d6a957a 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -1,39 +1,15 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function from ast import literal_eval from random import sample -from compas.datastructures import Graph -from compas.datastructures import Mesh -from compas.datastructures.attributes import CellAttributeView -from compas.datastructures.attributes import EdgeAttributeView -from compas.datastructures.attributes import FaceAttributeView -from compas.datastructures.attributes import VertexAttributeView +from compas.datastructures import Graph, Mesh +from compas.datastructures.attributes import CellAttributeView, EdgeAttributeView, FaceAttributeView, VertexAttributeView 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 -from compas.geometry import Vector -from compas.geometry import add_vectors -from compas.geometry import bestfit_plane -from compas.geometry import bounding_box -from compas.geometry import centroid_points -from compas.geometry import centroid_polygon -from compas.geometry import centroid_polyhedron -from compas.geometry import distance_point_point -from compas.geometry import length_vector -from compas.geometry import normal_polygon -from compas.geometry import normalize_vector -from compas.geometry import project_point_plane -from compas.geometry import scale_vector -from compas.geometry import subtract_vectors -from compas.geometry import volume_polyhedron +from compas.geometry import (Line, Plane, Point, Polygon, Polyhedron, Vector, add_vectors, bestfit_plane, bounding_box, centroid_points, centroid_polygon, centroid_polyhedron, + distance_point_point, length_vector, normal_polygon, normalize_vector, project_point_plane, scale_vector, subtract_vectors, volume_polyhedron) from compas.itertools import pairwise from compas.tolerance import TOL @@ -625,6 +601,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 do_faces_form_a_closed_cell(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. @@ -664,19 +661,9 @@ 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 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: - raise ValueError("Cannot add cell, faces {} can not be unified.".format(faces)) + 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 positive, we need to flip the faces to point inwards @@ -4181,6 +4168,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 # # -------------------------------------------------------------------------- From 37f08c0e1e509a213c3474db10ab8cda552e9153 Mon Sep 17 00:00:00 2001 From: Romana Rust Date: Sun, 26 May 2024 19:03:57 +0200 Subject: [PATCH 17/21] updates --- CHANGELOG.md | 8 ++ .../basics.datastructures.cellnetworks.py | 5 ++ .../cell_network/cell_network.py | 77 ++++++++++++------- 3 files changed, 63 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bd4235fe8c..f66c6a80dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.geometry.Brep.from_plane`. * Added `compas.tolerance.Tolerance.angulardeflection`. * Added `compas.scene.SceneObject.scene` attribute. +* Added `compas.datastructures.CellNetwork.do_faces_form_a_closed_cell` +* 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 diff --git a/docs/userguide/basics.datastructures.cellnetworks.py b/docs/userguide/basics.datastructures.cellnetworks.py index a4729ae41b2..97ae63272a4 100644 --- a/docs/userguide/basics.datastructures.cellnetworks.py +++ b/docs/userguide/basics.datastructures.cellnetworks.py @@ -9,6 +9,7 @@ cell_network = CellNetwork.from_json(compas.get("cellnetwork_example.json")) + viewer = Viewer(show_grid=False) no_cell = cell_network.faces_without_cell() for face in cell_network.faces(): @@ -22,4 +23,8 @@ 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() \ No newline at end of file diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 8aa1d6a957a..ef03d218fe7 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -1,15 +1,39 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function +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, Mesh -from compas.datastructures.attributes import CellAttributeView, EdgeAttributeView, FaceAttributeView, VertexAttributeView +from compas.datastructures import Graph +from compas.datastructures import Mesh +from compas.datastructures.attributes import CellAttributeView +from compas.datastructures.attributes import EdgeAttributeView +from compas.datastructures.attributes import FaceAttributeView +from compas.datastructures.attributes import VertexAttributeView from compas.datastructures.datastructure import Datastructure from compas.files import OBJ -from compas.geometry import (Line, Plane, Point, Polygon, Polyhedron, Vector, add_vectors, bestfit_plane, bounding_box, centroid_points, centroid_polygon, centroid_polyhedron, - distance_point_point, length_vector, normal_polygon, normalize_vector, project_point_plane, scale_vector, subtract_vectors, volume_polyhedron) +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 +from compas.geometry import Vector +from compas.geometry import add_vectors +from compas.geometry import bestfit_plane +from compas.geometry import bounding_box +from compas.geometry import centroid_points +from compas.geometry import centroid_polygon +from compas.geometry import centroid_polyhedron +from compas.geometry import distance_point_point +from compas.geometry import length_vector +from compas.geometry import normal_polygon +from compas.geometry import normalize_vector +from compas.geometry import project_point_plane +from compas.geometry import scale_vector +from compas.geometry import subtract_vectors +from compas.geometry import volume_polyhedron from compas.itertools import pairwise from compas.tolerance import TOL @@ -1058,7 +1082,7 @@ def cells_to_graph(self): 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])) + graph.add_edge(*sorted([cell, nbr])) return graph def cell_to_vertices_and_faces(self, cell): @@ -3998,30 +4022,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. + def cell_neighbors(self, cell): + """Find the neighbors of a given cell. - # Parameters - # ---------- - # cell : int - # The identifier of the 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. From 656cd48badf6a96ac149161e60bfef7c7a42acee Mon Sep 17 00:00:00 2001 From: Romana Rust <117177043+romanavyzn@users.noreply.github.com> Date: Mon, 27 May 2024 21:57:56 +0200 Subject: [PATCH 18/21] do_faces_form_a_closed_cell to is_faces_closed --- CHANGELOG.md | 2 +- docs/userguide/basics.datastructures.cellnetwork.rst | 10 ++++++---- src/compas/datastructures/cell_network/cell_network.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f66c6a80dc9..ffc5efa5e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.geometry.Brep.from_plane`. * Added `compas.tolerance.Tolerance.angulardeflection`. * Added `compas.scene.SceneObject.scene` attribute. -* Added `compas.datastructures.CellNetwork.do_faces_form_a_closed_cell` +* 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` diff --git a/docs/userguide/basics.datastructures.cellnetwork.rst b/docs/userguide/basics.datastructures.cellnetwork.rst index 95c09903b01..3ed0c35922c 100644 --- a/docs/userguide/basics.datastructures.cellnetwork.rst +++ b/docs/userguide/basics.datastructures.cellnetwork.rst @@ -33,9 +33,9 @@ From Scratch >>> 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]] ->>> [cell_network.add_vertex(x=x, y=y, z=z) for x, y, z in vertices] ->>> [cell_network.add_face(fverts) for fverts in faces] ->>> [cell_network.add_cell(fkeys) for fkeys in cells] +>>> 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) @@ -96,6 +96,8 @@ An edge can be assigned to any number of faces, or to none. [2, 3, 39] >>> cell_network.edge_faces((1, 10)) [8] +>>> cell_network.edge_faces((43, 34)) +[] >>> cell_network.edges_without_face() [(43, 34)] @@ -113,7 +115,7 @@ A face can be at maximum assigned to two cells, to one or None. A face is on the If all cells are connected, those faces form a closed cell as well: ->>> cell_network.do_faces_form_a_closed_cell(boundary) +>>> cell_network.is_faces_closed(boundary) True This shows only the faces on the boundary displayed. diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index ef03d218fe7..0ea76969ca1 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -639,7 +639,7 @@ def _faces_to_unified_mesh(self, faces): return None return mesh - def do_faces_form_a_closed_cell(self, faces): + def is_faces_closed(self, faces): """Checks if the faces form a closed cell.""" mesh = self._faces_to_unified_mesh(faces) if mesh: From 13408fe8012e09ba115083d49514b99b84af1e50 Mon Sep 17 00:00:00 2001 From: Romana Rust <117177043+romanavyzn@users.noreply.github.com> Date: Mon, 27 May 2024 22:00:13 +0200 Subject: [PATCH 19/21] Update basics.datastructures.cellnetwork.rst --- docs/userguide/basics.datastructures.cellnetwork.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/basics.datastructures.cellnetwork.rst b/docs/userguide/basics.datastructures.cellnetwork.rst index 3ed0c35922c..ccaef80dd8f 100644 --- a/docs/userguide/basics.datastructures.cellnetwork.rst +++ b/docs/userguide/basics.datastructures.cellnetwork.rst @@ -31,7 +31,7 @@ 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]] +>>> 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] From 54f11eea736cf169209a211c27be7bc4f9b598c8 Mon Sep 17 00:00:00 2001 From: Romana Rust <117177043+romanavyzn@users.noreply.github.com> Date: Mon, 27 May 2024 22:04:16 +0200 Subject: [PATCH 20/21] adding edge_data to data scheme --- src/compas/datastructures/cell_network/cell_network.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 0ea76969ca1..6e49e289e6d 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -131,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"}}, @@ -155,6 +160,7 @@ class CellNetwork(Datastructure): "edge", "face", "cell", + "edge_data", "face_data", "cell_data", "max_vertex", From 48c8619568b79a7f206f7c91014e77b8b720c5f0 Mon Sep 17 00:00:00 2001 From: Romana Rust <117177043+romanavyzn@users.noreply.github.com> Date: Mon, 27 May 2024 22:14:11 +0200 Subject: [PATCH 21/21] Update basics.datastructures.cellnetwork.rst --- docs/userguide/basics.datastructures.cellnetwork.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/userguide/basics.datastructures.cellnetwork.rst b/docs/userguide/basics.datastructures.cellnetwork.rst index ccaef80dd8f..fa6928bfc39 100644 --- a/docs/userguide/basics.datastructures.cellnetwork.rst +++ b/docs/userguide/basics.datastructures.cellnetwork.rst @@ -90,7 +90,7 @@ The cell network contains mixed topologic entities such as cells, faces, edges a >>> cell_network.number_of_cells() 6 -An edge can be assigned to any number of faces, or to none. +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] @@ -100,6 +100,7 @@ An edge can be assigned to any number of faces, or to none. [] >>> 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.