Skip to content

Commit

Permalink
Merge pull request #1413 from compas-dev/fix_unify_cyc
Browse files Browse the repository at this point in the history
update unify cycles
  • Loading branch information
romanarust authored Dec 13, 2024
2 parents d77f706 + fc9e736 commit 347e5ec
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 27 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

* Fixed `PluginNotInstalledError` when using `Brep.from_boolean_*` in Rhino.
* Expose the parameters `radius` and `nmax` from `compas.topology._face_adjacency` to `compas.topology.face_adjacency` and further propagate them to `unify_cycles` and `Mesh.unify_cycles`.
* Modify `face_adjacency` to avoid using `compas.topology._face_adjacency` by default when there are more than 100 faces, unless one of the parameters `radius`, `nmax` is passed
* Added support for `Polyline` as input for `compas_rhino.Brep.from_extrusion`.

### Removed
Expand Down
13 changes: 11 additions & 2 deletions src/compas/datastructures/mesh/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -2932,9 +2932,18 @@ def quads_to_triangles(self, check_angles=False):
del self.facedata[face]

# only reason this is here and not on the halfedge is because of the spatial tree
def unify_cycles(self, root=None):
def unify_cycles(self, root=None, nmax=None, max_distance=None):
"""Unify the cycles of the mesh.
Parameters
----------
root : int, optional
The key of the root face.
nmax : int, optional
The maximum number of neighboring faces to consider. If neither nmax nor max_distance is specified, all faces will be considered.
max_distance : float, optional
The max_distance of the search sphere for neighboring faces. If neither nmax nor max_distance is specified, all faces will be considered.
Returns
-------
None
Expand All @@ -2951,7 +2960,7 @@ def unify_cycles(self, root=None):
vertices = self.vertices_attributes("xyz")
faces = [[vertex_index[vertex] for vertex in self.face_vertices(face)] for face in self.faces()]

unify_cycles(vertices, faces)
unify_cycles(vertices, faces, root=root, nmax=nmax, max_distance=max_distance)

self.halfedge = {key: {} for key in self.vertices()}
for index, vertices in enumerate(faces):
Expand Down
66 changes: 41 additions & 25 deletions src/compas/topology/orientation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
from compas.topology import breadth_first_traverse


def _closest_faces(vertices, faces, nmax=10, radius=10.0):
def _closest_faces(vertices, faces, nmax, max_distance):
points = [centroid_points([vertices[index] for index in face]) for face in faces]

k = min(len(faces), nmax)
k = len(faces) if nmax is None else min(len(faces), nmax)

# determine the k closest faces for each face
# each item in "closest" is
Expand All @@ -21,22 +21,26 @@ def _closest_faces(vertices, faces, nmax=10, radius=10.0):
# [2] the distance between the test point and the face centroid

try:
import numpy as np
from scipy.spatial import cKDTree

tree = cKDTree(points)
distances, closest = tree.query(points, k=k, workers=-1)
if max_distance is None:
return closest

closest_within_distance = []
for i, closest_row in enumerate(closest):
idx = np.where(distances[i] < max_distance)[0]
closest_within_distance.append(closest_row[idx].tolist())
return closest_within_distance

except Exception:
try:
from Rhino.Geometry import Point3d # type: ignore
from Rhino.Geometry import RTree # type: ignore
from Rhino.Geometry import Sphere # type: ignore

except Exception:
from compas.geometry import KDTree

tree = KDTree(points)
closest = [tree.nearest_neighbors(point, k) for point in points]
closest = [[index for xyz, index, d in nnbrs] for nnbrs in closest]

else:
tree = RTree()

for i, point in enumerate(points):
Expand All @@ -48,20 +52,26 @@ def callback(sender, e):

closest = []
for i, point in enumerate(points):
sphere = Sphere(Point3d(*point), radius)
sphere = Sphere(Point3d(*point), max_distance)
data = []
tree.Search(sphere, callback, data)
closest.append(data)
return closest

else:
tree = cKDTree(points)
_, closest = tree.query(points, k=k, workers=-1)
except Exception:
from compas.geometry import KDTree

return closest
tree = KDTree(points)
closest = [tree.nearest_neighbors(point, k) for point in points]
if max_distance is None:
return closest
return [[index for xyz, index, d in nnbrs if d < max_distance] for nnbrs in closest]


def _face_adjacency(vertices, faces, nmax=10, radius=10.0):
closest = _closest_faces(vertices, faces, nmax=nmax, radius=radius)
def _face_adjacency(vertices, faces, nmax=None, max_distance=None):
if nmax is None and max_distance is None:
raise ValueError("Either nmax or max_distance should be specified.")
closest = _closest_faces(vertices, faces, nmax=nmax, max_distance=max_distance)

adjacency = {}

Expand Down Expand Up @@ -94,7 +104,7 @@ def _face_adjacency(vertices, faces, nmax=10, radius=10.0):
return adjacency


def face_adjacency(points, faces):
def face_adjacency(points, faces, nmax=None, max_distance=None):
"""Build a face adjacency dict.
Parameters
Expand All @@ -103,6 +113,10 @@ def face_adjacency(points, faces):
The vertex locations of the faces.
faces : list[list[int]]
The faces defined as list of indices in the points list.
nmax : int, optional
The maximum number of neighboring faces to consider. If neither nmax nor max_distance is specified, all faces will be considered.
max_distance : float, optional
The max_distance of the search sphere for neighboring faces. If neither nmax nor max_distance is specified, all faces will be considered.
Returns
-------
Expand All @@ -117,10 +131,8 @@ def face_adjacency(points, faces):
purely geometrical, but uses a spatial indexing tree to speed up the search.
"""
f = len(faces)

if f > 100:
return _face_adjacency(points, faces)
if nmax or max_distance:
return _face_adjacency(points, faces, nmax=nmax, max_distance=max_distance)

adjacency = {}

Expand Down Expand Up @@ -152,7 +164,7 @@ def face_adjacency(points, faces):
return adjacency


def unify_cycles(vertices, faces, root=None):
def unify_cycles(vertices, faces, root=None, nmax=None, max_distance=None):
"""Unify the cycle directions of all faces.
Unified cycle directions is a necessary condition for the data structure to
Expand All @@ -164,8 +176,12 @@ def unify_cycles(vertices, faces, root=None):
The vertex coordinates of the mesh.
faces : list[list[int]]
The faces of the mesh defined as lists of vertex indices.
root : str, optional
root : int, optional
The key of the root face.
nmax : int, optional
The maximum number of neighboring faces to consider. If neither nmax nor max_distance is specified, all faces will be considered.
max_distance : float, optional
The max_distance of the search sphere for neighboring faces. If neither nmax nor max_distance is specified, all faces will be considered.
Returns
-------
Expand Down Expand Up @@ -196,7 +212,7 @@ def unify(node, nbr):
if root is None:
root = random.choice(list(range(len(faces))))

adj = face_adjacency(vertices, faces) # this is the only place where the vertex coordinates are used
adj = face_adjacency(vertices, faces, nmax=nmax, max_distance=max_distance) # this is the only place where the vertex coordinates are used

visited = breadth_first_traverse(adj, root, unify)

Expand Down
Loading

0 comments on commit 347e5ec

Please sign in to comment.