Skip to content

Commit 605028f

Browse files
committed
refined contact detection
1 parent 09326bb commit 605028f

File tree

4 files changed

+130
-19
lines changed

4 files changed

+130
-19
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
* Added `compas_model.algorithms.contacts.brepface_brepface_overlap_holes` to compute the precise interface between two brep faces.
13+
1214
### Changed
1315

1416
* Fixed bug in `compas_model.elements.plate.Plate.compute_obb`.
1517
* Fixed bug in contact detection due to failing vector matching in `compas_model.algorithms.contacts`.
18+
* Changed `compas_model.algorithms.contacts.brep_brep_contacts` to use `brepface_brepface_overlap_holes` to refine the contact geometry of brepfaes that have already been found to be in contact.
19+
* Changed `compas_model.interactions.contact.Contact` to register holes in the contact geometry.
20+
* Changed `compas_model.interactions.contact.Contact` to compute a precise brep geometry of the contact, including holes if they are present.
1621

1722
### Removed
1823

src/compas_model/algorithms/contacts.py

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,14 @@ def mesh_mesh_contacts(
122122
if not is_opposite_normal_normal(a_normal, b_normal):
123123
continue
124124

125-
result = polygon_polygon_overlap(a_points, a_normal, b_points, b_normal, tolerance, minimum_area)
125+
result = polygon_polygon_overlap(a_points, b_points, a_normal, tolerance, minimum_area) # type: ignore
126126

127127
# this is not always an accurate representation of the interface
128128
# if the polygon has holes
129129
# the interface is incorrect
130130

131131
if result:
132-
points, frame, area = result
132+
points, frame, area, matrix_to_local, matrix_to_world = result
133133
contact = contacttype(points=points, frame=frame, size=area)
134134
contacts.append(contact)
135135

@@ -200,40 +200,71 @@ def brep_brep_contacts(
200200
if not is_opposite_normal_normal(a_normal, b_normal):
201201
continue
202202

203-
result = polygon_polygon_overlap(a_points, a_normal, b_points, b_normal, tolerance, minimum_area)
203+
result = polygon_polygon_overlap(a_points, b_points, a_normal, tolerance, minimum_area)
204204

205205
# this is not always an accurate representation of the interface
206206
# if the polygon has holes
207207
# the interface is incorrect
208208

209209
if result:
210-
# if a_face.area < b_face.area:
211-
# c = OCCBrep.from_brepfaces([a_face])
212-
# else:
213-
# c = OCCBrep.from_brepfaces([b_face])
210+
points, frame, area, matrix_to_local, matrix_to_world = result
214211

215-
points, frame, area = result
216-
contact = contacttype(points=points, frame=frame, size=area)
212+
# if the result exists, but the faces have holes
213+
# compute the holes in the intersection polygon
214+
215+
holes = brepface_brepface_overlap_holes(a_face, b_face, matrix_to_local, matrix_to_world, minimum_area)
216+
217+
contact = contacttype(points=points, frame=frame, size=area, holes=holes)
217218
contacts.append(contact)
218219

219220
return contacts
220221

221222

222-
def polygon_polygon_overlap(a_points, a_normal, b_points, b_normal, tolerance, minimum_area):
223+
def polygon_polygon_overlap(
224+
a_points: list[Point] | list[list[float]],
225+
b_points: list[Point] | list[list[float]],
226+
normal: Vector,
227+
tolerance: float,
228+
minimum_area: float,
229+
) -> Optional[tuple[list[Point], Frame, float, Transformation, Transformation]]:
230+
"""Compute the overlap between two polygons defined by their corner points.
231+
232+
Parameters
233+
----------
234+
a_points
235+
The corner points of the first polygon.
236+
b_points
237+
The corner points of the second polygon.
238+
normal
239+
The normal vector defining the desired orientation of the local coordinate frame.
240+
tolerance
241+
Maximum deviation from the perfectly flat interface plane.
242+
minimum_area
243+
Minimum area of the overlap polygon.
244+
245+
Returns
246+
-------
247+
tuple[list[Point], Frame, float, Transformation, Transformation] | None
248+
The corner points of the overlap polygon, the local coordinate frame, the area of the overlap polygon,
249+
the transformation to local coordinates, and the transformation to world coordinates.
250+
Returns None if there is no valid overlap.
251+
252+
"""
223253
# this ensures that a shared frame is used to do the interface calculations
224254
frame = Frame(*bestfit_frame_numpy(a_points + b_points))
225255

226256
# the frame should be oriented along the normal of the "a" face
227257
# this will align the interface frame with the resulting interaction edge
228258
# which is important for calculations with solvers such as CRA
229-
if frame.zaxis.dot(a_normal) < 0:
259+
if frame.zaxis.dot(normal) < 0:
230260
frame.invert()
231261

232262
# compute the transformation to frame coordinates
233-
matrix = Transformation.from_change_of_basis(Frame.worldXY(), frame)
263+
matrix_to_local = Transformation.from_change_of_basis(Frame.worldXY(), frame)
264+
matrix_to_world = matrix_to_local.inverted()
234265

235-
a_projected = transform_points(a_points, matrix)
236-
b_projected = transform_points(b_points, matrix)
266+
a_projected = transform_points(a_points, matrix_to_local)
267+
b_projected = transform_points(b_points, matrix_to_local)
237268

238269
p0 = ShapelyPolygon(a_projected)
239270
p1 = ShapelyPolygon(b_projected)
@@ -258,7 +289,65 @@ def polygon_polygon_overlap(a_points, a_normal, b_points, b_normal, tolerance, m
258289
return
259290

260291
coords = [[x, y, 0.0] for x, y, _ in intersection.exterior.coords]
261-
points = [Point(*xyz) for xyz in transform_points(coords, matrix.inverted())[:-1]]
292+
points = [Point(*xyz) for xyz in transform_points(coords, matrix_to_world)[:-1]]
293+
262294
frame = Frame(centroid_polygon(points), frame.xaxis, frame.yaxis)
263295

264-
return points, frame, area
296+
return points, frame, area, matrix_to_local, matrix_to_world
297+
298+
299+
def brepface_brepface_overlap_holes(
300+
a,
301+
b,
302+
matrix_to_local,
303+
matrix_to_world,
304+
minimum_area,
305+
) -> Optional[list[Polygon]]:
306+
"""Compute the holes in the overlap between two brep faces.
307+
308+
Parameters
309+
----------
310+
a : BrepFace
311+
The first brep face.
312+
b : BrepFace
313+
The second brep face.
314+
matrix_to_local : Transformation
315+
The transformation to local coordinates.
316+
matrix_to_world : Transformation
317+
The transformation to world coordinates.
318+
minimum_area : float
319+
Minimum area of a hole to be considered.
320+
321+
Returns
322+
-------
323+
list[Polygon] | None
324+
The holes in the overlap polygon, or None if there are no holes.
325+
326+
"""
327+
a_points = a.loops[0].to_polygon().points
328+
b_points = b.loops[0].to_polygon().points
329+
330+
a_holes = []
331+
if len(a.loops) > 1:
332+
for loop in a.loops[1:]:
333+
a_holes.append(transform_points(loop.to_polygon().points, matrix_to_local))
334+
335+
b_holes = []
336+
if len(b.loops) > 1:
337+
for loop in b.loops[1:]:
338+
b_holes.append(transform_points(loop.to_polygon().points, matrix_to_local))
339+
340+
a_shapely = ShapelyPolygon(transform_points(a_points, matrix_to_local), holes=a_holes)
341+
b_shapely = ShapelyPolygon(transform_points(b_points, matrix_to_local), holes=b_holes)
342+
343+
intersection: ShapelyPolygon = a_shapely.intersection(b_shapely) # type: ignore
344+
area = intersection.area
345+
346+
if area < minimum_area:
347+
# the interface area is too small
348+
return
349+
350+
holes = [[[x, y, 0.0] for x, y, _ in interior.coords] for interior in intersection.interiors]
351+
holes = [Polygon(transform_points(hole, matrix_to_world)[:-1]) for hole in holes]
352+
353+
return holes

src/compas_model/geometry/polygon.py

Whitespace-only changes.

src/compas_model/interactions/contact.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class Contact(Data):
2626
The total area of the contact polygon.
2727
mesh : Mesh, optional
2828
The mesh representation of the contact surface.
29+
holes : list[Polygon], optional
30+
Holes in the contact polygon.
2931
name : str, optional
3032
A human-readable name.
3133
@@ -40,7 +42,7 @@ class Contact(Data):
4042
points : list[Point]
4143
The corner points of the interface polygon.
4244
polygon : Polygon
43-
The interfaces polygon.
45+
The interface polygon.
4446
size : float
4547
The area of the interface polygon.
4648
@@ -53,11 +55,12 @@ class Contact(Data):
5355
@property
5456
def __data__(self) -> dict:
5557
return {
58+
"name": self.name,
5659
"points": self.points,
5760
"frame": self._frame,
5861
"size": self._size,
5962
"mesh": self._mesh,
60-
"name": self.name,
63+
"holes": self._holes,
6164
}
6265

6366
def __init__(
@@ -66,6 +69,7 @@ def __init__(
6669
frame: Optional[Frame] = None,
6770
size: Optional[float] = None,
6871
mesh: Optional[Mesh] = None,
72+
holes: Optional[list[Polygon]] = None,
6973
name: Optional[str] = None,
7074
):
7175
super().__init__(name)
@@ -75,6 +79,7 @@ def __init__(
7579
self._mesh = mesh
7680
self._brep = None
7781
self._polygon = Polygon(points)
82+
self._holes = holes
7883

7984
@property
8085
def polygon(self) -> Polygon:
@@ -105,7 +110,19 @@ def mesh(self) -> Mesh:
105110
@property
106111
def brep(self) -> Brep:
107112
if self._brep is None:
108-
self._brep = Brep.from_polygons([self.polygon])
113+
if self._holes:
114+
from compas_occ.brep import OCCBrepFace
115+
from compas_occ.brep import OCCBrepLoop
116+
117+
face = OCCBrepFace.from_polygon(self.polygon)
118+
loops = []
119+
for hole in self._holes:
120+
loop = OCCBrepLoop.from_polygon(hole)
121+
loops.append(loop)
122+
face.add_loops(loops)
123+
self._brep = Brep.from_brepfaces([face])
124+
else:
125+
self._brep = Brep.from_polygons([self.polygon])
109126
return self._brep
110127

111128
@property

0 commit comments

Comments
 (0)