Skip to content

Commit be55894

Browse files
authored
Merge pull request #803 from compas-dev/thickening-direction
Update offset module
2 parents ec77a5f + 96b03ab commit be55894

File tree

3 files changed

+84
-20
lines changed

3 files changed

+84
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010

1111
### Added
1212

13+
* Added Python 3.9 support.
1314
* Added crease handling to catmull-clark subdivision scheme.
14-
* Added Python 3.9 support
1515
* Added `compas_ghpython.get_grasshopper_userobjects_path` to retrieve User Objects target folder.
16+
* Added direction option for mesh thickening.
17+
* Added check for closed meshes.
1618

1719
### Changed
1820

src/compas/datastructures/mesh/core/halfedge.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,17 +1739,34 @@ def is_quadmesh(self):
17391739
return not any(4 != len(self.face_vertices(fkey)) for fkey in self.faces())
17401740

17411741
def is_empty(self):
1742-
"""Boolean whether the mesh is empty.
1742+
"""Verify that the mesh is empty.
17431743
17441744
Returns
17451745
-------
17461746
bool
1747-
True if no vertices. False otherwise.
1747+
True if the mesh has no vertices.
1748+
False otherwise.
17481749
"""
17491750
if self.number_of_vertices() == 0:
17501751
return True
17511752
return False
17521753

1754+
def is_closed(self):
1755+
"""Verify that the mesh is closed.
1756+
1757+
Returns
1758+
-------
1759+
bool
1760+
True if the mesh is not empty and has no naked edges.
1761+
False otherwise.
1762+
"""
1763+
if self.is_empty():
1764+
return False
1765+
for edge in self.edges():
1766+
if self.is_edge_on_boundary(*edge):
1767+
return False
1768+
return True
1769+
17531770
def euler(self):
17541771
"""Calculate the Euler characteristic.
17551772

src/compas/datastructures/mesh/offset.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,51 +14,96 @@
1414
]
1515

1616

17-
def mesh_offset(mesh, distance=1.0,):
17+
def mesh_offset(mesh, distance=1.0):
1818
"""Offset a mesh.
1919
2020
Parameters
2121
----------
22-
mesh : Mesh
22+
mesh : :class:`compas.datastructures.Mesh`
2323
A Mesh to offset.
24-
distance : float
24+
distance : float, optional
2525
The offset distance.
26+
Default is ``1.0``.
2627
2728
Returns
2829
-------
29-
Mesh
30+
:class:`compas.datastructures.Mesh`
3031
The offset mesh.
3132
33+
Notes
34+
-----
35+
If the offset distance is a positive value, the offset is in the direction of the vertex normal.
36+
If the value is negative, the offset is in the opposite direction.
37+
In both cases, the orientation of the offset mesh is the same as the orientation of the original.
38+
39+
In areas with high degree of curvature, the offset mesh can have self-intersections.
40+
41+
Examples
42+
--------
43+
>>> from compas.datastructures import Mesh, mesh_offset
44+
>>> from compas.geometry import distance_point_point as dist
45+
>>> mesh = Mesh.from_vertices_and_faces([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], [[0, 1, 2, 3]])
46+
>>> offset = mesh_offset(mesh)
47+
>>> all(dist(mesh.vertex_coordinates(a), offset.vertex_coordinates(b)) == 1 for a, b in zip(mesh.vertices(), offset.vertices()))
48+
True
49+
3250
"""
3351
offset = mesh.copy()
3452

35-
for key in offset.vertices():
36-
normal = mesh.vertex_normal(key)
37-
xyz = mesh.vertex_coordinates(key)
38-
offset.vertex_attributes(key, 'xyz', add_vectors(xyz, scale_vector(normal, distance)))
53+
for vertex in offset.vertices():
54+
normal = mesh.vertex_normal(vertex)
55+
xyz = mesh.vertex_coordinates(vertex)
56+
offset.vertex_attributes(vertex, 'xyz', add_vectors(xyz, scale_vector(normal, distance)))
3957

4058
return offset
4159

4260

43-
def mesh_thicken(mesh, thickness=1.0):
61+
def mesh_thicken(mesh, thickness=1.0, both=True):
4462
"""Thicken a mesh.
4563
4664
Parameters
4765
----------
48-
mesh : Mesh
66+
mesh : :class:`compas.datastructures.Mesh`
4967
A mesh to thicken.
50-
thickness : real
51-
The mesh thickness
68+
thickness : float, optional
69+
The mesh thickness.
70+
This should be a positive value.
71+
Default is ``1.0``.
72+
both : bool, optional
73+
If true, the mesh is thickened on both sides of the original.
74+
Otherwise, the mesh is thickened on the side of the positive normal.
5275
5376
Returns
5477
-------
55-
thickened_mesh : Mesh
78+
thickened_mesh : :class:`compas.datastructures.Mesh`
5679
The thickened mesh.
5780
81+
Raises
82+
------
83+
ValueError
84+
If ``thickness`` is not a positive number.
85+
86+
Examples
87+
--------
88+
>>> from compas.datastructures import Mesh, mesh_thicken
89+
>>> from compas.geometry import distance_point_point as dist
90+
>>> mesh = Mesh.from_vertices_and_faces([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], [[0, 1, 2, 3]])
91+
>>> thick = mesh_thicken(mesh)
92+
>>> thick.is_closed()
93+
True
94+
>>> all(dist(mesh.vertex_coordinates(a), thick.vertex_coordinates(b)) == 1 for a, b in zip(mesh.vertices(), thick.vertices()))
95+
True
96+
5897
"""
59-
# offset in both directions
60-
mesh_top = mesh_offset(mesh, +0.5 * thickness)
61-
mesh_bottom = mesh_offset(mesh, -0.5 * thickness)
98+
if thickness <= 0:
99+
raise ValueError("Thickness should be a positive number.")
100+
101+
if both:
102+
mesh_top = mesh_offset(mesh, +0.5 * thickness)
103+
mesh_bottom = mesh_offset(mesh, -0.5 * thickness)
104+
else:
105+
mesh_top = mesh_offset(mesh, thickness)
106+
mesh_bottom = mesh.copy()
62107

63108
# flip bottom part
64109
mesh_flip_cycles(mesh_bottom)
@@ -85,4 +130,4 @@ def mesh_thicken(mesh, thickness=1.0):
85130
if __name__ == '__main__':
86131

87132
import doctest
88-
doctest.testmod(globs=globals())
133+
doctest.testmod()

0 commit comments

Comments
 (0)