Skip to content

Commit 58ac4d3

Browse files
authored
Merge pull request #923 from compas-dev/fix-mesh-strip-and-loop
Fix bug in continuous loops and strips
2 parents c751b9f + eeddb70 commit 58ac4d3

File tree

4 files changed

+194
-88
lines changed

4 files changed

+194
-88
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added halfedge loops in `compas.datastructures.Halfedge.halfedge_loop`.
13+
* Added halfedge strips in `compas.datastructures.Halfedge.halfedge_strip`.
1214
* Added boundingbox to `compas_rhino.conduits.BaseConduit`
1315

1416
### Changed
1517

1618
* Fixed bug in combination of `compas_rhino.artists.MeshArtist.draw_mesh` and `compas_rhino.utilities.drawing.draw_mesh`.
19+
* Fixed bug in continuous loops in `compas.datastructures.Halfedge.edge_loop`.
20+
* Fixed bug in continuous strips in `compas.datastructures.Halfedge.edge_strip`.
1721
* Changed abstract method `compas.artists.MeshArtist.draw_mesh` to implemented method in `compas_plotters.artists.MeshArtist.draw_mesh`.
1822

1923
### Removed

src/compas/datastructures/halfedge/halfedge.py

Lines changed: 65 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2092,76 +2092,75 @@ def edge_loop(self, edge):
20922092
list of tuple of int
20932093
The edges on the same loop as the given edge.
20942094
"""
2095-
if self.is_edge_on_boundary(*edge):
2096-
return self._edge_loop_on_boundary(edge)
2097-
edges = []
2098-
current, previous = edge
2099-
edges.append((previous, current))
2095+
u, v = edge
2096+
uv_loop = self.halfedge_loop((u, v))
2097+
if uv_loop[0][0] == uv_loop[-1][1]:
2098+
return uv_loop
2099+
vu_loop = self.halfedge_loop((v, u))
2100+
vu_loop[:] = [(u, v) for v, u in vu_loop[::-1]]
2101+
return vu_loop + uv_loop[1:]
2102+
2103+
def halfedge_loop(self, edge):
2104+
"""Find all edges on the same loop as the halfedge, in the direction of the halfedge.
2105+
2106+
Parameters
2107+
----------
2108+
edge : tuple of int
2109+
The identifier of the starting edge.
2110+
2111+
Returns
2112+
-------
2113+
list of tuple of int
2114+
The edges on the same loop as the given edge.
2115+
"""
2116+
u, v = edge
2117+
if self.is_edge_on_boundary(u, v):
2118+
return self._halfedge_loop_on_boundary(edge)
2119+
edges = [(u, v)]
21002120
while True:
2101-
if current == edge[1]:
2102-
break
2103-
nbrs = self.vertex_neighbors(current, ordered=True)
2121+
nbrs = self.vertex_neighbors(v, ordered=True)
21042122
if len(nbrs) != 4:
21052123
break
2106-
i = nbrs.index(previous)
2107-
previous = current
2108-
current = nbrs[i - 2]
2109-
edges.append((previous, current))
2110-
edges[:] = [(u, v) for v, u in edges[::-1]]
2111-
if edges[0][0] == edges[-1][1]:
2112-
return edges
2113-
previous, current = edge
2114-
while True:
2115-
nbrs = self.vertex_neighbors(current, ordered=True)
2116-
if len(nbrs) != 4:
2124+
i = nbrs.index(u)
2125+
u = v
2126+
v = nbrs[i - 2]
2127+
edges.append((u, v))
2128+
if v == edges[0][0]:
21172129
break
2118-
i = nbrs.index(previous)
2119-
previous = current
2120-
current = nbrs[i - 2]
2121-
edges.append((previous, current))
21222130
return edges
21232131

2124-
def _edge_loop_on_boundary(self, uv):
2125-
"""Find all edges on the same loop as a given edge on the boundary."""
2126-
edges = []
2127-
current, previous = uv
2128-
edges.append((previous, current))
2132+
def _halfedge_loop_on_boundary(self, edge):
2133+
"""Find all edges on the same loop as the halfedge, in the direction of the halfedge, if the halfedge is on the boundary.
2134+
2135+
Parameters
2136+
----------
2137+
edge : tuple of int
2138+
The identifier of the starting edge.
2139+
2140+
Returns
2141+
-------
2142+
list of tuple of int
2143+
The edges on the same loop as the given edge.
2144+
"""
2145+
u, v = edge
2146+
edges = [(u, v)]
21292147
while True:
2130-
if current == uv[1]:
2131-
break
2132-
nbrs = self.vertex_neighbors(current)
2148+
nbrs = self.vertex_neighbors(v)
21332149
if len(nbrs) == 2:
21342150
break
21352151
nbr = None
21362152
for temp in nbrs:
2137-
if temp == previous:
2153+
if temp == u:
21382154
continue
2139-
if self.is_edge_on_boundary(current, temp):
2155+
if self.is_edge_on_boundary(v, temp):
21402156
nbr = temp
21412157
break
21422158
if nbr is None:
21432159
break
2144-
previous, current = current, nbr
2145-
edges.append((previous, current))
2146-
edges[:] = [(u, v) for v, u in edges[::-1]]
2147-
if edges[0][0] == edges[-1][1]:
2148-
return edges
2149-
previous, current = uv
2150-
while True:
2151-
nbrs = self.vertex_neighbors(current)
2152-
if len(nbrs) == 2:
2153-
break
2154-
nbr = None
2155-
for temp in nbrs:
2156-
if temp == previous:
2157-
continue
2158-
if self.is_edge_on_boundary(current, temp):
2159-
nbr = temp
2160-
break
2161-
if nbr is None:
2160+
u, v = v, nbr
2161+
edges.append((u, v))
2162+
if v == edges[0][0]:
21622163
break
2163-
previous, current = current, nbr
2164-
edges.append((previous, current))
21652164
return edges
21662165

21672166
def edge_strip(self, edge):
@@ -2177,33 +2176,18 @@ def edge_strip(self, edge):
21772176
list of tuple of int
21782177
The edges on the same strip as the given edge.
21792178
"""
2180-
edges = []
2181-
v, u = edge
2182-
while True:
2183-
edges.append((u, v))
2184-
face = self.halfedge[u][v]
2185-
if face is None:
2186-
break
2187-
vertices = self.face_vertices(face)
2188-
if len(vertices) != 4:
2189-
break
2190-
i = vertices.index(u)
2191-
u = vertices[i - 1]
2192-
v = vertices[i - 2]
2193-
edges[:] = [(u, v) for v, u in edges[::-1]]
21942179
u, v = edge
2195-
while True:
2196-
face = self.halfedge[u][v]
2197-
if face is None:
2198-
break
2199-
vertices = self.face_vertices(face)
2200-
if len(vertices) != 4:
2201-
break
2202-
i = vertices.index(u)
2203-
u = vertices[i - 1]
2204-
v = vertices[i - 2]
2205-
edges.append((u, v))
2206-
return edges
2180+
if self.halfedge[v][u] is None:
2181+
return self.halfedge_strip((u, v))
2182+
if self.halfedge[u][v] is None:
2183+
edges = self.halfedge_strip((v, u))
2184+
return [(u, v) for v, u in edges[::-1]]
2185+
vu_strip = self.halfedge_strip((v, u))
2186+
vu_strip[:] = [(u, v) for v, u in vu_strip[::-1]]
2187+
if vu_strip[0] == vu_strip[-1]:
2188+
return vu_strip
2189+
uv_strip = self.halfedge_strip((u, v))
2190+
return vu_strip[:-1] + uv_strip
22072191

22082192
def halfedge_strip(self, edge):
22092193
"""Find all edges on the same strip as a given halfedge.
@@ -2231,6 +2215,8 @@ def halfedge_strip(self, edge):
22312215
u = vertices[i - 1]
22322216
v = vertices[i - 2]
22332217
edges.append((u, v))
2218+
if (u, v) == edge:
2219+
break
22342220
return edges
22352221

22362222
# --------------------------------------------------------------------------

src/compas/datastructures/mesh/mesh.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from compas.geometry import vector_average
3636

3737
from compas.utilities import linspace
38-
from compas.utilities import meshgrid
3938

4039
from compas.utilities import geometric_key
4140
from compas.utilities import pairwise
@@ -639,16 +638,15 @@ def from_meshgrid(cls, dx, nx, dy=None, ny=None):
639638
dy = dy or dx
640639
ny = ny or nx
641640

642-
U, V = meshgrid(linspace(0, dx, nx + 1), linspace(0, dy, ny + 1), indexing='ij')
643-
644-
quads = [[
645-
[U[i + 0][j + 0], V[i + 0][j + 0], 0.0],
646-
[U[i + 0][j + 1], V[i + 0][j + 1], 0.0],
647-
[U[i + 1][j + 1], V[i + 1][j + 1], 0.0],
648-
[U[i + 1][j + 0], V[i + 1][j + 0], 0.0]
641+
vertices = [[x, y, 0.0] for x, y in product(linspace(0, dx, nx + 1), linspace(0, dy, ny + 1))]
642+
faces = [[
643+
i * (ny + 1) + j,
644+
(i + 1) * (ny + 1) + j,
645+
(i + 1) * (ny + 1) + j + 1,
646+
i * (ny + 1) + j + 1
649647
] for i, j in product(range(nx), range(ny))]
650648

651-
return cls.from_polygons(quads)
649+
return cls.from_vertices_and_faces(vertices, faces)
652650

653651
# --------------------------------------------------------------------------
654652
# helpers

tests/compas/datastructures/test_halfedge.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import pytest
2+
import random
23

34
import compas
5+
6+
from compas.geometry import Sphere
7+
48
from compas.datastructures import HalfEdge
9+
from compas.datastructures import Mesh
510

611

712
# ==============================================================================
@@ -35,6 +40,19 @@ def edge_key():
3540
return (0, 1)
3641

3742

43+
@pytest.fixture
44+
def sphere():
45+
sphere = Sphere([0, 0, 0], 1.0)
46+
mesh = Mesh.from_shape(sphere, u=16, v=16)
47+
return mesh
48+
49+
50+
@pytest.fixture
51+
def grid():
52+
mesh = Mesh.from_meshgrid(dx=10, nx=10)
53+
return mesh
54+
55+
3856
# ==============================================================================
3957
# Tests - Schema & jsonschema
4058
# ==============================================================================
@@ -240,3 +258,103 @@ def test_del_edge_attribute_in_view(mesh, edge_key):
240258
del attrs["foo"]
241259
with pytest.raises(KeyError):
242260
attrs["foo"]
261+
262+
263+
# ==============================================================================
264+
# Tests - Loops & Strip
265+
# ==============================================================================
266+
267+
def test_loops_and_strips_closed(sphere):
268+
poles = list(sphere.vertices_where({'vertex_degree': 16}))
269+
270+
for nbr in sphere.vertex_neighbors(poles[0]):
271+
meridian = sphere.edge_loop((poles[0], nbr))
272+
273+
assert len(meridian) == 16, meridian
274+
assert meridian[0][0] == poles[0]
275+
assert meridian[-1][1] == poles[1]
276+
277+
for edge in meridian[1:-1]:
278+
strip = sphere.edge_strip(edge)
279+
280+
assert len(strip) == 17, strip
281+
assert strip[0] == strip[-1]
282+
283+
for edge in meridian[1:-1]:
284+
ring = sphere.edge_loop(sphere.halfedge_before(*edge))
285+
286+
assert len(ring) == 16, ring
287+
assert ring[0][0] == ring[-1][1]
288+
289+
290+
def test_loops_and_strips_open(grid):
291+
assert grid.number_of_edges() == 220
292+
293+
edge = 47, 48
294+
strip = grid.edge_strip(edge)
295+
loop = grid.edge_loop(edge)
296+
297+
assert edge in strip
298+
assert len(strip) == 11
299+
assert grid.is_edge_on_boundary(* strip[0])
300+
assert grid.is_edge_on_boundary(* strip[-1])
301+
302+
assert edge in loop
303+
assert len(loop) == 10
304+
assert grid.is_vertex_on_boundary(loop[0][0])
305+
assert grid.is_vertex_on_boundary(loop[-1][1])
306+
307+
308+
def test_loops_and_strips_open_corner(grid):
309+
assert grid.number_of_edges() == 220
310+
311+
edge = 0, 1
312+
loop = grid.edge_loop(edge)
313+
strip = grid.edge_strip(edge)
314+
315+
assert edge in strip
316+
assert len(strip) == 11
317+
assert grid.is_edge_on_boundary(* strip[0])
318+
assert grid.is_edge_on_boundary(* strip[-1])
319+
assert edge == strip[-1]
320+
321+
assert edge in loop
322+
assert len(loop) == 10
323+
assert edge == loop[0]
324+
325+
edge = 1, 0
326+
loop = grid.edge_loop(edge)
327+
strip = grid.edge_strip(edge)
328+
329+
assert edge in strip
330+
assert len(strip) == 11
331+
assert grid.is_edge_on_boundary(* strip[0])
332+
assert grid.is_edge_on_boundary(* strip[-1])
333+
assert edge == strip[0]
334+
335+
assert edge in loop
336+
assert len(loop) == 10
337+
assert edge == loop[-1]
338+
339+
340+
def test_loops_and_strips_open_boundary(grid):
341+
assert grid.number_of_edges() == 220
342+
343+
edge = random.choice(grid.edges_on_boundary())
344+
u, v = edge
345+
346+
loop = grid.edge_loop(edge)
347+
strip = grid.edge_strip(edge)
348+
349+
assert edge in strip
350+
assert len(strip) == 11
351+
assert grid.is_edge_on_boundary(* strip[0])
352+
assert grid.is_edge_on_boundary(* strip[-1])
353+
354+
assert edge in loop
355+
assert len(loop) == 10
356+
357+
if grid.halfedge[u][v] is None:
358+
assert edge == strip[-1]
359+
else:
360+
assert edge == strip[0]

0 commit comments

Comments
 (0)