compas-model 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ from __future__ import print_function
2
+
3
+ import os
4
+
5
+ __author__ = ["petras vestartas"]
6
+ __copyright__ = "Block Research Group - ETH Zurich"
7
+ __license__ = "MIT License"
8
+ __email__ = "petrasvestartas@gmail.com"
9
+ __version__ = "0.3.0"
10
+
11
+ HERE = os.path.dirname(__file__)
12
+
13
+ HOME = os.path.abspath(os.path.join(HERE, "../../"))
14
+ DATA = os.path.abspath(os.path.join(HOME, "data"))
15
+ DOCS = os.path.abspath(os.path.join(HOME, "docs"))
16
+ TEMP = os.path.abspath(os.path.join(HOME, "temp"))
17
+
18
+ __all__ = ["HOME", "DATA", "DOCS", "TEMP"]
19
+
20
+ __all_plugins__ = [
21
+ "compas_model.scene",
22
+ "compas_model.rhino.scene",
23
+ "compas_model.notebook.scene",
24
+ ]
@@ -0,0 +1,12 @@
1
+ from .collider import is_aabb_aabb_collision
2
+ from .collider import is_box_box_collision
3
+ from .collider import is_face_to_face_collision
4
+ from .collider import get_collision_pairs
5
+
6
+
7
+ __all__ = [
8
+ "is_aabb_aabb_collision",
9
+ "is_box_box_collision",
10
+ "is_face_to_face_collision",
11
+ "get_collision_pairs",
12
+ ]
@@ -0,0 +1,353 @@
1
+ from math import fabs
2
+
3
+ from compas.geometry import Frame
4
+ from compas.geometry import Plane
5
+ from compas.geometry import Polygon
6
+ from compas.geometry import Transformation
7
+ from compas.geometry import bestfit_plane
8
+ from compas.geometry import distance_point_point
9
+ from compas.geometry import transform_points
10
+
11
+ try:
12
+ from shapely.geometry import Polygon as ShapelyPolygon
13
+
14
+ shapely_available = True
15
+ except ImportError:
16
+ print("Shapely package is not available. Please install it.")
17
+ shapely_available = False
18
+
19
+ import compas_model.model # noqa: F401
20
+
21
+
22
+ def is_aabb_aabb_collision(box0, box1):
23
+ """Verify if this axis-aligned bounding-box collides with another axis-aligned bounding-box.
24
+
25
+ Parameters
26
+ ----------
27
+ box0 : :class:`compas.geometry.Box`
28
+ First axis-aligned bounding-box.
29
+ box1 : :class:`compas.geometry.Box`
30
+ Second axis-aligned bounding-box.
31
+
32
+ Returns
33
+ -------
34
+ bool
35
+ True if the two axis-aligned bounding-box collide.
36
+ False otherwise.
37
+
38
+ """
39
+
40
+ p0_0 = box0.xmin, box0.ymin, box0.zmin
41
+ p0_1 = box0.xmax, box0.ymax, box0.zmax
42
+ p1_0 = box1.xmin, box1.ymin, box1.zmin
43
+ p1_1 = box1.xmax, box1.ymax, box1.zmax
44
+
45
+ if p0_1[0] < p1_0[0] or p1_1[0] < p0_0[0]:
46
+ return False
47
+
48
+ if p0_1[1] < p1_0[1] or p1_1[1] < p0_0[1]:
49
+ return False
50
+
51
+ if p0_1[2] < p1_0[2] or p1_1[2] < p0_0[2]:
52
+ return False
53
+
54
+ return True
55
+
56
+
57
+ def is_box_box_collision(box0, box1):
58
+ """Verify if this box collides with another box.
59
+
60
+ Parameters
61
+ ----------
62
+ box0 : :class:`compas.geometry.Box`
63
+ First box.
64
+ box1 : :class:`compas.geometry.Box`
65
+ Second box.
66
+
67
+ Returns
68
+ -------
69
+ bool
70
+ True if the two boxes collide.
71
+ False otherwise.
72
+
73
+ """
74
+
75
+ def get_separation_plane(relative_position, axis, box0, box1):
76
+ return abs(relative_position.dot(axis)) > (
77
+ abs((box0.frame.xaxis * box0.width * 0.5).dot(axis))
78
+ + abs((box0.frame.yaxis * box0.depth * 0.5).dot(axis))
79
+ + abs((box0.frame.zaxis * box0.height * 0.5).dot(axis))
80
+ + abs((box1.frame.xaxis * box1.width * 0.5).dot(axis))
81
+ + abs((box1.frame.yaxis * box1.depth * 0.5).dot(axis))
82
+ + abs((box1.frame.zaxis * box1.height * 0.5).dot(axis))
83
+ )
84
+
85
+ relative_position = box1.frame.point - box0.frame.point
86
+
87
+ result = not (
88
+ get_separation_plane(relative_position, box0.frame.xaxis, box0, box1)
89
+ or get_separation_plane(relative_position, box0.frame.yaxis, box0, box1)
90
+ or get_separation_plane(relative_position, box0.frame.zaxis, box0, box1)
91
+ or get_separation_plane(relative_position, box1.frame.xaxis, box0, box1)
92
+ or get_separation_plane(relative_position, box1.frame.yaxis, box0, box1)
93
+ or get_separation_plane(relative_position, box1.frame.zaxis, box0, box1)
94
+ or get_separation_plane(relative_position, box0.frame.xaxis.cross(box1.frame.xaxis), box0, box1)
95
+ or get_separation_plane(relative_position, box0.frame.xaxis.cross(box1.frame.yaxis), box0, box1)
96
+ or get_separation_plane(relative_position, box0.frame.xaxis.cross(box1.frame.zaxis), box0, box1)
97
+ or get_separation_plane(relative_position, box0.frame.yaxis.cross(box1.frame.xaxis), box0, box1)
98
+ or get_separation_plane(relative_position, box0.frame.yaxis.cross(box1.frame.yaxis), box0, box1)
99
+ or get_separation_plane(relative_position, box0.frame.yaxis.cross(box1.frame.zaxis), box0, box1)
100
+ or get_separation_plane(relative_position, box0.frame.zaxis.cross(box1.frame.xaxis), box0, box1)
101
+ or get_separation_plane(relative_position, box0.frame.zaxis.cross(box1.frame.yaxis), box0, box1)
102
+ or get_separation_plane(relative_position, box0.frame.zaxis.cross(box1.frame.zaxis), box0, box1)
103
+ )
104
+
105
+ return result
106
+
107
+
108
+ def is_face_to_face_collision(
109
+ polygons0,
110
+ polygons1,
111
+ frames0=None,
112
+ frames1=None,
113
+ tolerance_flatness=1e-2,
114
+ tolerance_area=1e1,
115
+ log=False,
116
+ ):
117
+ """Construct interfaces by intersecting coplanar mesh faces.
118
+
119
+ Parameters
120
+ ----------
121
+ assembly : compas_assembly.datastructures.Assembly
122
+ An assembly of discrete blocks.
123
+ nmax : int, optional
124
+ Maximum number of neighbours per block.
125
+ tolerance_flatness : float, optional
126
+ Maximum deviation from the perfectly flat interface plane.
127
+ tolerance_area : float, optional
128
+ Minimum area of a "face-face" interface.
129
+ log : bool, optional
130
+ Log the conversion process, here the algorithms mostly fails due to user wrong inputs.
131
+
132
+ Returns
133
+ -------
134
+ Polygon of the Interface - :class:`compas.geometry.Polygon`
135
+ Current Element ID - list[int]
136
+ Other Element ID - list[int]
137
+ Current Element Face Index - int
138
+ Other Element Face Index - int
139
+ """
140
+
141
+ if shapely_available is False:
142
+ return []
143
+
144
+ _frames0 = frames0
145
+ _frames1 = frames1
146
+
147
+ if _frames0 is None and _frames1 is None:
148
+ _frames0 = [Frame.from_plane(Plane(*bestfit_plane(polygon))) for polygon in polygons0]
149
+ _frames1 = [Frame.from_plane(Plane(*bestfit_plane(polygon))) for polygon in polygons1]
150
+
151
+ interfaces = []
152
+
153
+ for id_0, face_polygon_0 in enumerate(polygons0):
154
+ matrix = Transformation.from_frame_to_frame(_frames0[id_0].copy(), Frame.worldXY())
155
+
156
+ shapely_polygon_0 = _to_shapely_polygon(matrix, face_polygon_0, tolerance_flatness, tolerance_area, log)
157
+ if shapely_polygon_0 is None:
158
+ if log:
159
+ print("Collider -> is_face_to_face_collision -> shapely_polygon_0 is None, frame or polygon is bad")
160
+ continue
161
+
162
+ for id_1, face_polygon_1 in enumerate(polygons1):
163
+ if _is_parallel_and_coplanar(_frames0[id_0], _frames1[id_1]) is False:
164
+ continue
165
+
166
+ shapely_polygon_1 = _to_shapely_polygon(matrix, face_polygon_1, tolerance_flatness, tolerance_area, log)
167
+ if shapely_polygon_1 is None:
168
+ continue
169
+
170
+ if not shapely_polygon_0.intersects(shapely_polygon_1):
171
+ continue
172
+
173
+ intersection = shapely_polygon_0.intersection(shapely_polygon_1)
174
+ area = intersection.area
175
+ if area < tolerance_area:
176
+ continue
177
+
178
+ polygon = _to_compas_polygon(matrix, intersection)
179
+ interfaces.append([(id_0, id_1), polygon])
180
+
181
+ return interfaces
182
+
183
+
184
+ def _to_shapely_polygon(matrix, polygon, tolerance_flatness=1e-3, tolerance_area=1e-1, log=False):
185
+ """Convert a compas polygon to shapely polygon on xy plane.
186
+
187
+ Parameters
188
+ ----------
189
+ matrix : :class:`compas.geometry.Transformation`
190
+ Transformation matrix to transform the polygon to the xy plane.
191
+ polygon : :class:`compas.geometry.Polygon`
192
+ Compas polygon.
193
+ tolerance_flatness : float, optional
194
+ Tolerance for the planarity of the polygon.
195
+ tolerance_area : float, optional
196
+ Tolerance for the area of the polygon.
197
+ log : bool, optional
198
+ Log the conversion process, here the algorithms mostly fails due to user wrong inputs.
199
+
200
+ Returns
201
+ -------
202
+ :class:`shapely.geometry.Polygon`
203
+ Shapely polygon on the xy plane.
204
+
205
+ """
206
+
207
+ # Orient points to the xy plane.
208
+ projected = transform_points(polygon.points, matrix)
209
+
210
+ # Check the planarity and the area of the polygon.
211
+ if not all(fabs(point[2]) < tolerance_flatness for point in projected):
212
+ if log:
213
+ print("collider -> to_shapely_polygon: the polygon planarity is above the tolerance_flatness.")
214
+ return None
215
+ elif polygon.area < tolerance_area:
216
+ if log:
217
+ print(
218
+ "collider -> is_face_to_face_collision -> "
219
+ + "to_shapely_polygon: the polygon area is smaller than tolerance_area. "
220
+ + str(polygon.area)
221
+ + " < "
222
+ + str(tolerance_area)
223
+ )
224
+ return None
225
+ else:
226
+ return ShapelyPolygon(projected)
227
+
228
+
229
+ def _to_compas_polygon(matrix, shapely_polygon):
230
+ """Convert a shapely polygon to compas polygon back to the frame.
231
+
232
+ Parameters
233
+ ----------
234
+ matrix : :class:`compas.geometry.Transformation`
235
+ Transformation matrix to transform the polygon to the original frame.
236
+ shapely_polygon : :class:`shapely.geometry.Polygon`
237
+ Shapely polygon on the xy plane.
238
+
239
+ Returns
240
+ -------
241
+ :class:`compas.geometry.Polygon`
242
+
243
+ """
244
+
245
+ coords = [[x, y, 0.0] for x, y, _ in shapely_polygon.exterior.coords]
246
+ return Polygon(transform_points(coords, matrix.inverted())[:-1])
247
+
248
+
249
+ def _is_parallel_and_coplanar(
250
+ frame0,
251
+ frame1,
252
+ tolerance_normal_colinearity=1e-1,
253
+ tolerance_projection_distance=1e-1,
254
+ ):
255
+ """Check if two frames are coplanar and at the same position
256
+
257
+ Parameters
258
+ ----------
259
+ frame0 : :class:`compas.geometry.Frame`
260
+ First frame.
261
+ frame1 : :class:`compas.geometry.Frame`
262
+ Second frame.
263
+ tolerance_normal_colinearity : float, optional
264
+ Tolerance for the colinearity of the normals.
265
+ tolerance_projection_distance : float, optional
266
+ Tolerance for the distance between the projected points.
267
+
268
+ Returns
269
+ -------
270
+ bool
271
+ True if the two frames are coplanar and at the same position.
272
+ False otherwise.
273
+
274
+ """
275
+
276
+ # Are the two normals are parallel?
277
+ are_parellel = abs(frame0.normal.cross(frame1.normal).length) < tolerance_normal_colinearity
278
+
279
+ # Are planes at the same positions?
280
+ projected_point = Plane(frame0.point, frame0.normal).projected_point(frame1.point)
281
+ are_close = distance_point_point(projected_point, frame1.point) < tolerance_projection_distance
282
+ return are_parellel and are_close
283
+
284
+
285
+ def get_collision_pairs(
286
+ model,
287
+ aabb_and_obb_inflation=0.01,
288
+ obb_obb=True,
289
+ face_to_face=True,
290
+ tolerance_flatness=1e-2,
291
+ tolerance_area=1e1,
292
+ log=False,
293
+ ):
294
+ # type: (compas_model.model.Model, float, bool, bool, float, float, bool) -> list
295
+ """Get the collision pairs of the elements in the model.
296
+
297
+ Parameters
298
+ ----------
299
+ model : :class:`compas_model.model.Model`
300
+ Model of the assembly.
301
+ aabb_and_obb_inflation : float, optional
302
+ Inflation of the axis-aligned bounding-box and oriented bounding-box.
303
+ obb_obb : bool, optional
304
+ Verify the collision between oriented bounding-boxes.
305
+ face_to_face : bool, optional
306
+ Verify the collision between the faces of the elements.
307
+ has_orientation : bool, optional
308
+ Consider the polygon winding.
309
+ tolerance_flatness : float, optional
310
+ Maximum deviation from the perfectly flat interface plane.
311
+ tolerance_area : float, optional
312
+ Minimum area of a "face-face" interface.
313
+ log : bool, optional
314
+ Log the conversion process, here the algorithms mostly fails due to user wrong inputs.
315
+
316
+ Returns
317
+ -------
318
+ list[[int, int, [(int, int), :class:`compas.geometry.Polygon`]]
319
+ List of collision pairs. Each collision pair is a list of two element IDs and a list of interface polygons.
320
+
321
+ """
322
+
323
+ elements = model.elementlist
324
+
325
+ for e in elements:
326
+ e.compute_aabb(aabb_and_obb_inflation)
327
+ e.compute_obb(aabb_and_obb_inflation)
328
+
329
+ collision_pairs = []
330
+ for i in range(len(elements)):
331
+ for j in range(i + 1, len(elements)):
332
+ if not is_aabb_aabb_collision(elements[i].aabb, elements[j].aabb):
333
+ continue
334
+
335
+ if not obb_obb or is_box_box_collision(elements[i].obb, elements[j].obb):
336
+ if not face_to_face:
337
+ collision_pairs.append([i, j])
338
+ else:
339
+ interfaces = is_face_to_face_collision(
340
+ elements[i].face_polygons,
341
+ elements[j].face_polygons,
342
+ None,
343
+ None,
344
+ tolerance_flatness,
345
+ tolerance_area,
346
+ log,
347
+ )
348
+ if interfaces:
349
+ result = [i, j]
350
+ result.append(interfaces)
351
+ collision_pairs.append(result)
352
+
353
+ return collision_pairs
@@ -0,0 +1,16 @@
1
+ from .element import Element
2
+ from .element import Feature
3
+ from .block import BlockElement
4
+ from .block import BlockFeature
5
+ from .interface import InterfaceElement
6
+ from .interface import InterfaceFeature
7
+
8
+
9
+ __all__ = [
10
+ "Element",
11
+ "Feature",
12
+ "BlockElement",
13
+ "BlockFeature",
14
+ "InterfaceElement",
15
+ "InterfaceFeature",
16
+ ]
@@ -0,0 +1,116 @@
1
+ import compas.datastructures # noqa: F401
2
+ from compas.datastructures import Mesh
3
+ from compas.geometry import Box
4
+ from compas.geometry import bounding_box
5
+ from compas.geometry import convex_hull_numpy
6
+ from compas.geometry import oriented_bounding_box
7
+
8
+ from compas_model.elements import Element
9
+ from compas_model.elements import Feature
10
+
11
+
12
+ # A block could have features like notches,
13
+ # but we will work on it when we need it...
14
+ # A notch could be a cylinder defined in the frame of a face.
15
+ # The frame of a face should be defined in coorination with the global frame of the block.
16
+ # during interface detection the features could/should be ignored.
17
+ class BlockFeature(Feature):
18
+ pass
19
+
20
+
21
+ class BlockElement(Element):
22
+ """Class representing block elements.
23
+
24
+ Parameters
25
+ ----------
26
+ shape : :class:`compas.datastructures.Mesh`
27
+ The base shape of the block.
28
+ features : list[:class:`BlockFeature`], optional
29
+ Additional block features.
30
+ is_support : bool, optional
31
+ Flag indicating that the block is a support.
32
+ frame : :class:`compas.geometry.Frame`, optional
33
+ The coordinate frame of the block.
34
+ name : str, optional
35
+ The name of the element.
36
+
37
+ Attributes
38
+ ----------
39
+ shape : :class:`compas.datastructure.Mesh`
40
+ The base shape of the block.
41
+ features : list[:class:`BlockFeature`]
42
+ A list of additional block features.
43
+ is_support : bool
44
+ Flag indicating that the block is a support.
45
+
46
+ """
47
+
48
+ @property
49
+ def __data__(self):
50
+ # type: () -> dict
51
+ data = super(BlockElement, self).__data__
52
+ data["shape"] = self.shape
53
+ data["features"] = self.features
54
+ data["is_support"] = self.is_support
55
+ return data
56
+
57
+ def __init__(self, shape, features=None, is_support=False, frame=None, name=None):
58
+ # type: (Mesh, list[BlockFeature] | None, bool, compas.geometry.Frame | None, str | None) -> None
59
+
60
+ super(BlockElement, self).__init__(frame=frame, name=name)
61
+ self.shape = shape
62
+ self.features = features or [] # type: list[BlockFeature]
63
+ self.is_support = is_support
64
+
65
+ # don't like this
66
+ # but want to test the collider
67
+ @property
68
+ def face_polygons(self):
69
+ # type: () -> list[compas.geometry.Polygon]
70
+ return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore
71
+
72
+ # =============================================================================
73
+ # Implementations of abstract methods
74
+ # =============================================================================
75
+
76
+ def compute_geometry(self, include_features=False):
77
+ geometry = self.shape
78
+ if include_features:
79
+ if self.features:
80
+ for feature in self.features:
81
+ geometry = feature.apply(geometry)
82
+ geometry.transform(self.worldtransformation)
83
+ return geometry
84
+
85
+ def compute_aabb(self, inflate=0.0):
86
+ points = self.geometry.vertices_attributes("xyz") # type: ignore
87
+ box = Box.from_bounding_box(bounding_box(points))
88
+ box.xsize += inflate
89
+ box.ysize += inflate
90
+ box.zsize += inflate
91
+ return box
92
+
93
+ def compute_obb(self, inflate=0.0):
94
+ points = self.geometry.vertices_attributes("xyz") # type: ignore
95
+ box = Box.from_bounding_box(oriented_bounding_box(points))
96
+ box.xsize += inflate
97
+ box.ysize += inflate
98
+ box.zsize += inflate
99
+ return box
100
+
101
+ def compute_collision_mesh(self):
102
+ points = self.geometry.vertices_attributes("xyz") # type: ignore
103
+ vertices, faces = convex_hull_numpy(points)
104
+ vertices = [points[index] for index in vertices] # type: ignore
105
+ return Mesh.from_vertices_and_faces(vertices, faces)
106
+
107
+ # =============================================================================
108
+ # Constructors
109
+ # =============================================================================
110
+
111
+ @classmethod
112
+ def from_box(cls, box):
113
+ # type: (compas.geometry.Box) -> BlockElement
114
+ shape = box.to_mesh()
115
+ block = cls(shape=shape)
116
+ return block