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.
- compas_model/__init__.py +24 -0
- compas_model/algorithms/__init__.py +12 -0
- compas_model/algorithms/collider.py +353 -0
- compas_model/elements/__init__.py +16 -0
- compas_model/elements/block.py +116 -0
- compas_model/elements/element.py +344 -0
- compas_model/elements/interface.py +99 -0
- compas_model/elements/plate.py +153 -0
- compas_model/interactions/__init__.py +16 -0
- compas_model/interactions/interaction.py +24 -0
- compas_model/materials/__init__.py +0 -0
- compas_model/model/__init__.py +14 -0
- compas_model/model/elementnode.py +45 -0
- compas_model/model/elementtree.py +81 -0
- compas_model/model/groupnode.py +71 -0
- compas_model/model/interactiongraph.py +102 -0
- compas_model/model/model.py +434 -0
- compas_model/notebook/__init__.py +0 -0
- compas_model/notebook/scene/__init__.py +27 -0
- compas_model/notebook/scene/blockobject.py +177 -0
- compas_model/notebook/scene/elementobject.py +10 -0
- compas_model/notebook/scene/modelobject.py +28 -0
- compas_model/rhino/__init__.py +0 -0
- compas_model/scene/__init__.py +24 -0
- compas_model/scene/blockobject.py +8 -0
- compas_model/scene/elementobject.py +109 -0
- compas_model/scene/modelobject.py +79 -0
- compas_model-0.3.0.dist-info/LICENSE +21 -0
- compas_model-0.3.0.dist-info/METADATA +84 -0
- compas_model-0.3.0.dist-info/RECORD +32 -0
- compas_model-0.3.0.dist-info/WHEEL +5 -0
- compas_model-0.3.0.dist-info/top_level.txt +1 -0
compas_model/__init__.py
ADDED
|
@@ -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
|