Skip to content

Commit 51efbed

Browse files
authored
Merge pull request #903 from compas-dev/shape-api
Improved API for shape booleans
2 parents b043bf2 + 58f93c5 commit 51efbed

File tree

13 files changed

+320
-89
lines changed

13 files changed

+320
-89
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
* Added `draw_vertexlabels`, `draw_edgelabels`, `draw_facelabels`, `draw_vertexnormals`, and `draw_facenormals` to `compas_blender.artists.MeshArtist`.
13+
* Added optional `triangulated` flag to `to_vertices_and_faces` of all shapes.
14+
* Added `compas.geometry.Geometry` base class.
15+
* Added `__add__`, `__sub__`, `__and__` to `compas.geometry.Shape` for boolean operations using binary operators.
16+
* Added `is_closed` to `compas.geometry.Polyhedron`.
1317
* Added `Plane.offset`.
1418
* Added `draw_node_labels` and `draw_edgelabels` to `compas_blender.artists.NetworkArtist`.
1519
* Added `compas_blender.artists.RobotModelArtist.clear`.
1620

1721
### Changed
1822

1923
* Fixed bug in `compas_blender.draw_texts`.
24+
* Changed default resolution for shape discretisation to 16 for both u and v where relevant.
25+
* Changed base class of `compas.geometry.Primitive` and `compas.geometry.Shape` to `compas.geometry.Geometry`.
2026
* `compas_blender.artists.RobotModelArtist.collection` can be assigned as a Blender collection or a name.
2127
* Generalized the parameter `color` of `compas_blender.draw_texts` and various label drawing methods.
2228

src/compas/geometry/__init__.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
66
.. currentmodule:: compas.geometry
77
8+
Base Class
9+
==========
10+
11+
.. autosummary::
12+
:toctree: generated/
13+
:nosignatures:
14+
15+
Geometry
16+
817
Classes
918
=======
1019
@@ -828,6 +837,9 @@
828837
world_to_local_coordinates_numpy,
829838
local_to_world_coordinates_numpy
830839
)
840+
841+
from .geometry import Geometry
842+
831843
from .primitives import ( # noqa: E402
832844
Primitive,
833845
Bezier,
@@ -1201,9 +1213,8 @@
12011213
'trimesh_remesh_along_isoline',
12021214
'trimesh_slice',
12031215

1204-
'KDTree',
1216+
'Geometry',
12051217

1206-
'Pointcloud',
12071218
'Primitive',
12081219
'Bezier',
12091220
'Circle',
@@ -1226,6 +1237,9 @@
12261237
'Sphere',
12271238
'Torus',
12281239

1240+
'Pointcloud',
1241+
'KDTree',
1242+
12291243
'Projection',
12301244
'Reflection',
12311245
'Rotation',

src/compas/geometry/geometry.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import absolute_import
2+
from __future__ import division
3+
from __future__ import print_function
4+
5+
from compas.data import Data
6+
7+
8+
class Geometry(Data):
9+
"""Base class for all geometric objects."""
10+
11+
def __ne__(self, other):
12+
# this is not obvious to ironpython
13+
return not self.__eq__(other)
14+
15+
def transform(self, transformation):
16+
"""Transform the geometry.
17+
18+
Parameters
19+
----------
20+
transformation : :class:`Transformation`
21+
The transformation used to transform the geometry.
22+
23+
Returns
24+
-------
25+
None
26+
27+
"""
28+
raise NotImplementedError
29+
30+
def transformed(self, transformation):
31+
"""Returns a transformed copy of this geometry.
32+
33+
Parameters
34+
----------
35+
transformation : :class:`Transformation`
36+
The transformation used to transform the geometry.
37+
38+
Returns
39+
-------
40+
:class:`Geometry`
41+
The transformed geometry.
42+
"""
43+
primitive = self.copy()
44+
primitive.transform(transformation)
45+
return primitive

src/compas/geometry/primitives/_primitive.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,8 @@
22
from __future__ import division
33
from __future__ import print_function
44

5-
from compas.data import Data
5+
from ..geometry import Geometry
66

77

8-
class Primitive(Data):
8+
class Primitive(Geometry):
99
"""Base class for geometric primitives."""
10-
11-
def __ne__(self, other):
12-
# this is not obvious to ironpython
13-
return not self.__eq__(other)
14-
15-
def transform(self, transformation):
16-
"""Transform the primitive.
17-
18-
Parameters
19-
----------
20-
transformation : :class:`Transformation`
21-
The transformation used to transform the Box.
22-
23-
Returns
24-
-------
25-
None
26-
27-
"""
28-
raise NotImplementedError
29-
30-
def transformed(self, transformation):
31-
"""Returns a transformed copy of this primitive.
32-
33-
Parameters
34-
----------
35-
transformation : :class:`Transformation`
36-
The transformation used to transform the primitive.
37-
38-
Returns
39-
-------
40-
:class:`Primitive`
41-
The transformed primitive.
42-
"""
43-
primitive = self.copy()
44-
primitive.transform(transformation)
45-
return primitive

src/compas/geometry/shapes/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,3 @@
1111
from .polyhedron import Polyhedron # noqa: F401
1212
from .sphere import Sphere # noqa: F401
1313
from .torus import Torus # noqa: F401
14-
15-
16-
__all__ = [name for name in dir() if not name.startswith('_')]

src/compas/geometry/shapes/_shape.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,115 @@
33
from __future__ import division
44

55
import abc
6-
from compas.geometry import Primitive
6+
from ..geometry import Geometry
77

88

9-
class Shape(Primitive):
9+
class Shape(Geometry):
1010
"""Base class for geometric shapes."""
1111

1212
@abc.abstractmethod
1313
def to_vertices_and_faces(self):
1414
pass
15+
16+
def __add__(self, other):
17+
"""Compute the boolean union using the "+" operator of this shape and another.
18+
19+
Parameters
20+
----------
21+
other : :class:`Solid`
22+
The solid to add.
23+
24+
Returns
25+
-------
26+
:class:`Solid`
27+
The resulting solid.
28+
29+
Examples
30+
--------
31+
>>> from compas.geometry import Box, Sphere
32+
>>> A = Box.from_width_height_depth(2, 2, 2)
33+
>>> B = Sphere([1, 1, 1], 1.0)
34+
>>> C = A + B
35+
"""
36+
from compas.geometry import boolean_union_mesh_mesh
37+
from compas.geometry import Polyhedron
38+
A = self.to_vertices_and_faces(triangulated=True)
39+
B = other.to_vertices_and_faces(triangulated=True)
40+
V, F = boolean_union_mesh_mesh(A, B)
41+
return Polyhedron(V, F)
42+
43+
def __sub__(self, other):
44+
"""Compute the boolean difference using the "-" operator of this shape and another.
45+
46+
Parameters
47+
----------
48+
other : :class:`Solid`
49+
The solid to subtract.
50+
51+
Returns
52+
-------
53+
:class:`Solid`
54+
The resulting solid.
55+
56+
Examples
57+
--------
58+
>>> from compas.geometry import Box, Sphere
59+
>>> A = Box.from_width_height_depth(2, 2, 2)
60+
>>> B = Sphere([1, 1, 1], 1.0)
61+
>>> C = A - B
62+
"""
63+
from compas.geometry import boolean_difference_mesh_mesh
64+
from compas.geometry import Polyhedron
65+
A = self.to_vertices_and_faces(triangulated=True)
66+
B = other.to_vertices_and_faces(triangulated=True)
67+
V, F = boolean_difference_mesh_mesh(A, B)
68+
return Polyhedron(V, F)
69+
70+
def __and__(self, other):
71+
"""Compute the boolean intersection using the "&" operator of this shape and another.
72+
73+
Parameters
74+
----------
75+
other : :class:`Solid`
76+
The solid to intersect with.
77+
78+
Returns
79+
-------
80+
:class:`Solid`
81+
The resulting solid.
82+
83+
Examples
84+
--------
85+
>>> from compas.geometry import Box, Sphere
86+
>>> A = Box.from_width_height_depth(2, 2, 2)
87+
>>> B = Sphere([1, 1, 1], 1.0)
88+
>>> C = A & B
89+
"""
90+
from compas.geometry import boolean_intersection_mesh_mesh
91+
from compas.geometry import Polyhedron
92+
A = self.to_vertices_and_faces(triangulated=True)
93+
B = other.to_vertices_and_faces(triangulated=True)
94+
V, F = boolean_intersection_mesh_mesh(A, B)
95+
return Polyhedron(V, F)
96+
97+
def __or__(self, other):
98+
"""Compute the boolean union using the "|" operator of this shape and another.
99+
100+
Parameters
101+
----------
102+
other : :class:`Solid`
103+
The solid to add.
104+
105+
Returns
106+
-------
107+
:class:`Solid`
108+
The resulting solid.
109+
110+
Examples
111+
--------
112+
>>> from compas.geometry import Box, Sphere
113+
>>> A = Box.from_width_height_depth(2, 2, 2)
114+
>>> B = Sphere([1, 1, 1], 1.0)
115+
>>> C = A | B
116+
"""
117+
return self.__add__(other)

src/compas/geometry/shapes/box.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from compas.geometry import Frame
99
from compas.geometry import Vector
1010

11-
from compas.geometry.shapes._shape import Shape
11+
from ._shape import Shape
1212

1313

1414
class Box(Shape):
@@ -519,6 +519,29 @@ def from_diagonal(cls, diagonal):
519519
# methods
520520
# ==========================================================================
521521

522+
def to_vertices_and_faces(self, triangulated=False):
523+
"""Returns a list of vertices and faces.
524+
525+
Parameters
526+
----------
527+
triangulated: bool, optional
528+
Flag indicating that the faces have to be triangulated.
529+
530+
Returns
531+
-------
532+
(vertices, faces)
533+
A list of vertex locations and a list of faces,
534+
with each face defined as a list of indices into the list of vertices.
535+
"""
536+
if triangulated:
537+
faces = []
538+
for a, b, c, d in self.faces:
539+
faces.append([a, b, c])
540+
faces.append([a, c, d])
541+
else:
542+
faces = self.faces
543+
return self.vertices, faces
544+
522545
def contains(self, point):
523546
"""Verify if the box contains a given point.
524547
@@ -538,17 +561,6 @@ def contains(self, point):
538561
return True
539562
return False
540563

541-
def to_vertices_and_faces(self):
542-
"""Returns a list of vertices and faces.
543-
544-
Returns
545-
-------
546-
(vertices, faces)
547-
A list of vertex locations and a list of faces,
548-
with each face defined as a list of indices into the list of vertices.
549-
"""
550-
return self.vertices, self.faces
551-
552564
def transform(self, transformation):
553565
"""Transform the box.
554566

src/compas/geometry/shapes/capsule.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from compas.geometry import Plane
1313
from compas.geometry import Line
1414

15-
from compas.geometry.shapes import Shape
15+
from ._shape import Shape
1616

1717

1818
class Capsule(Shape):
@@ -186,17 +186,17 @@ def from_data(cls, data):
186186
# methods
187187
# ==========================================================================
188188

189-
def to_vertices_and_faces(self, u=10, v=10):
189+
def to_vertices_and_faces(self, u=16, v=16, triangulated=False):
190190
"""Returns a list of vertices and faces.
191191
192192
Parameters
193193
----------
194194
u : int, optional
195195
Number of faces in the 'u' direction.
196-
Default is ``10``.
197196
v : int, optional
198197
Number of faces in the 'v' direction.
199-
Default is ``10``.
198+
triangulated: bool, optional
199+
Flag indicating that the faces have to be triangulated.
200200
201201
Returns
202202
-------
@@ -263,6 +263,16 @@ def to_vertices_and_faces(self, u=10, v=10):
263263
nn = len(vertices) - 3 - (j + 1) % u
264264
faces.append([np, nn, nc])
265265

266+
if triangulated:
267+
triangles = []
268+
for face in faces:
269+
if len(face) == 4:
270+
triangles.append(face[0:3])
271+
triangles.append([face[0], face[2], face[3]])
272+
else:
273+
triangles.append(face)
274+
faces = triangles
275+
266276
return vertices, faces
267277

268278
def transform(self, transformation):

0 commit comments

Comments
 (0)