ifctrano 0.1.12__tar.gz → 0.3.0__tar.gz

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.
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: ifctrano
3
- Version: 0.1.12
3
+ Version: 0.3.0
4
4
  Summary: Package for generating building energy simulation model from IFC
5
- Home-page: https://github.com/andoludo/ifctrano
6
5
  License: GPL V3
7
6
  Keywords: BIM,IFC,energy simulation,modelica,building energy simulation,buildings,ideas
8
7
  Author: Ando Andriamamonjy
@@ -12,11 +11,13 @@ Classifier: License :: Other/Proprietary License
12
11
  Classifier: Programming Language :: Python :: 3
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
15
  Requires-Dist: ifcopenshell (>=0.8.1.post1,<0.9.0)
16
- Requires-Dist: pydantic (>=2.10.6,<3.0.0)
16
+ Requires-Dist: open3d (>=0.19.0,<0.20.0)
17
17
  Requires-Dist: shapely (>=2.0.7,<3.0.0)
18
- Requires-Dist: trano (>=0.1.50,<0.2.0)
18
+ Requires-Dist: trano (>=0.5.0,<0.6.0)
19
19
  Requires-Dist: typer (>=0.12.5,<0.13.0)
20
+ Requires-Dist: vedo (>=2025.5.3,<2026.0.0)
20
21
  Project-URL: Repository, https://github.com/andoludo/ifctrano
21
22
  Description-Content-Type: text/markdown
22
23
 
@@ -0,0 +1,3 @@
1
+ import warnings
2
+
3
+ warnings.filterwarnings("ignore", category=RuntimeWarning)
@@ -0,0 +1,439 @@
1
+ import json
2
+ import math
3
+ from itertools import combinations
4
+ from multiprocessing import Process
5
+ from pathlib import Path
6
+ from typing import Tuple, Literal, List, Annotated, Any, Dict, cast
7
+
8
+ import ifcopenshell.geom
9
+ import numpy as np
10
+ import open3d # type: ignore
11
+ from numpy import ndarray
12
+ from pydantic import (
13
+ BaseModel,
14
+ BeforeValidator,
15
+ ConfigDict,
16
+ model_validator,
17
+ computed_field,
18
+ )
19
+ from shapely.geometry.polygon import Polygon # type: ignore
20
+ from vedo import Line, Arrow, Mesh, show, write # type: ignore
21
+
22
+ from ifctrano.exceptions import VectorWithNansError
23
+
24
+ settings = ifcopenshell.geom.settings() # type: ignore
25
+ Coordinate = Literal["x", "y", "z"]
26
+ AREA_TOLERANCE = 0.5
27
+ ROUNDING_FACTOR = 5
28
+ CLASH_CLEARANCE = 0.5
29
+
30
+
31
+ class BaseModelConfig(BaseModel):
32
+ model_config = ConfigDict(arbitrary_types_allowed=True)
33
+
34
+
35
+ def round_two_decimals(value: float) -> float:
36
+ return round(value, 10)
37
+
38
+
39
+ def _show(lines: List[Line], interactive: bool = True) -> None:
40
+ show(
41
+ *lines,
42
+ axes=1,
43
+ viewup="z",
44
+ bg="white",
45
+ interactive=interactive,
46
+ )
47
+
48
+
49
+ class BaseShow(BaseModel):
50
+ model_config = ConfigDict(arbitrary_types_allowed=True)
51
+
52
+ def lines(self) -> List[Line]: ... # type: ignore
53
+
54
+ def description(self) -> Any: ... # noqa: ANN401
55
+
56
+ def show(self, interactive: bool = True) -> None:
57
+ p = Process(target=_show, args=(self.lines(), interactive))
58
+ p.start()
59
+
60
+ def write(self) -> None:
61
+
62
+ write(
63
+ *self.lines(),
64
+ axes=1,
65
+ viewup="z",
66
+ bg="white",
67
+ interactive=True,
68
+ )
69
+
70
+ @classmethod
71
+ def load_description(cls, file_path: Path) -> Dict[str, Any]:
72
+ return cast(Dict[str, Any], json.loads(file_path.read_text()))
73
+
74
+ def save_description(self, file_path: Path) -> None:
75
+ file_path.write_text(json.dumps(sorted(self.description()), indent=4))
76
+
77
+ def description_loaded(self) -> Dict[str, Any]:
78
+ return cast(Dict[str, Any], json.loads(json.dumps(sorted(self.description()))))
79
+
80
+
81
+ class BasePoint(BaseModel):
82
+ x: Annotated[float, BeforeValidator(round_two_decimals)]
83
+ y: Annotated[float, BeforeValidator(round_two_decimals)]
84
+ z: Annotated[float, BeforeValidator(round_two_decimals)]
85
+
86
+ @classmethod
87
+ def from_coordinate(cls, point: Tuple[float, float, float]) -> "BasePoint":
88
+ return cls(x=point[0], y=point[1], z=point[2])
89
+
90
+ def to_array(self) -> np.ndarray: # type: ignore
91
+ return np.array([self.x, self.y, self.z])
92
+
93
+ def to_list(self) -> List[float]:
94
+ return [self.x, self.y, self.z]
95
+
96
+ def to_tuple(self) -> Tuple[float, float, float]:
97
+ return (self.x, self.y, self.z)
98
+
99
+ @classmethod
100
+ def from_array(cls, array: np.ndarray) -> "BasePoint": # type: ignore
101
+ try:
102
+ return cls(x=array[0], y=array[1], z=array[2])
103
+ except IndexError as e:
104
+ raise ValueError("Array must have three components") from e
105
+
106
+ def __eq__(self, other: "BasePoint") -> bool: # type: ignore
107
+ return all([self.x == other.x, self.y == other.y, self.z == other.z])
108
+
109
+
110
+ Signs = Literal[-1, 1]
111
+
112
+
113
+ class Sign(BaseModel):
114
+ x: Signs = 1
115
+ y: Signs = 1
116
+ z: Signs = 1
117
+
118
+ def __hash__(self) -> int:
119
+ return hash((self.x, self.y, self.z))
120
+
121
+
122
+ class Vector(BasePoint):
123
+
124
+ @model_validator(mode="after")
125
+ def _validator(self) -> "Vector":
126
+ if any(np.isnan(v) for v in self.to_list()):
127
+ raise VectorWithNansError("Vector cannot have NaN values")
128
+ return self
129
+
130
+ def __mul__(self, other: "Vector") -> "Vector":
131
+
132
+ array = np.cross(self.to_array(), other.to_array())
133
+ return Vector(x=array[0], y=array[1], z=array[2])
134
+
135
+ def dot(self, other: "Vector") -> float:
136
+ return np.dot(self.to_array(), other.to_array()) # type: ignore
137
+
138
+ def angle(self, other: "Vector") -> int:
139
+ dot_product = np.dot(self.to_xy(), other.to_xy())
140
+ cross_product = np.cross(self.to_xy(), other.to_xy())
141
+ angle_rad = np.arctan2(cross_product, dot_product)
142
+ angle_deg = np.degrees(angle_rad)
143
+ if angle_deg < 0:
144
+ angle_deg += 360
145
+ return int(angle_deg)
146
+
147
+ def project(self, other: "Vector") -> "Vector":
148
+ a = self.dot(other) / other.dot(other)
149
+ return Vector(x=a * other.x, y=a * other.y, z=a * other.z)
150
+
151
+ def normalize(self) -> "Vector":
152
+ normalized_vector = self.to_array() / np.linalg.norm(self.to_array())
153
+ return Vector(
154
+ x=normalized_vector[0], y=normalized_vector[1], z=normalized_vector[2]
155
+ )
156
+
157
+ def norm(self) -> float:
158
+ return float(np.linalg.norm(self.to_array()))
159
+
160
+ def to_array(self) -> np.ndarray: # type: ignore
161
+ return np.array([self.x, self.y, self.z])
162
+
163
+ def to_xy(self) -> np.ndarray: # type: ignore
164
+ return np.array([self.x, self.y])
165
+
166
+ def get_normal_index(self) -> int:
167
+ normal_index_list = [abs(v) for v in self.to_list()]
168
+ return normal_index_list.index(max(normal_index_list))
169
+
170
+ def is_null(self, tolerance: float = 0.1) -> bool:
171
+ return all(abs(value) < tolerance for value in self.to_list())
172
+
173
+ @classmethod
174
+ def from_array(cls, array: np.ndarray) -> "Vector": # type: ignore
175
+ return cls.model_validate(super().from_array(array).model_dump())
176
+
177
+
178
+ class Point(BasePoint):
179
+ def __sub__(self, other: "Point") -> Vector:
180
+
181
+ return Vector(x=self.x - other.x, y=self.y - other.y, z=self.z - other.z)
182
+
183
+ def __add__(self, other: "Point") -> "Point":
184
+ return Point(x=self.x + other.x, y=self.y + other.y, z=self.z + other.z)
185
+
186
+ def s(self, signs: Sign) -> "Point":
187
+ return Point(x=self.x * signs.x, y=self.y * signs.y, z=self.z * signs.z)
188
+
189
+
190
+ class P(Point):
191
+ pass
192
+
193
+
194
+ class GlobalId(BaseModelConfig):
195
+ global_id: str
196
+
197
+
198
+ class CoordinateSystem(BaseModel):
199
+ x: Vector
200
+ y: Vector
201
+ z: Vector
202
+
203
+ def __eq__(self, other: "CoordinateSystem") -> bool: # type: ignore
204
+ return all(
205
+ [
206
+ self.x == other.x,
207
+ self.y == other.y,
208
+ self.z == other.z,
209
+ ]
210
+ )
211
+
212
+ @classmethod
213
+ def from_array(cls, array: np.ndarray) -> "CoordinateSystem": # type: ignore
214
+ return cls(
215
+ x=Vector.from_array(array[0]),
216
+ y=Vector.from_array(array[1]),
217
+ z=Vector.from_array(array[2]),
218
+ )
219
+
220
+ def to_array(self) -> np.ndarray: # type: ignore
221
+ return np.array([self.x.to_array(), self.y.to_array(), self.z.to_array()])
222
+
223
+ def inverse(self, array: np.array) -> np.array: # type: ignore
224
+ return np.round(np.dot(array, self.to_array()), ROUNDING_FACTOR) # type: ignore
225
+
226
+ def project(self, array: np.array) -> np.ndarray: # type: ignore
227
+ return np.round(np.dot(array, np.linalg.inv(self.to_array())), ROUNDING_FACTOR) # type: ignore
228
+
229
+
230
+ class Vertices(BaseModel):
231
+ points: List[Point]
232
+
233
+ @classmethod
234
+ def from_arrays(
235
+ cls, arrays: np.ndarray[tuple[int, ...], np.dtype[np.float64]]
236
+ ) -> "Vertices":
237
+ return cls(
238
+ points=[Point(x=array[0], y=array[1], z=array[2]) for array in arrays]
239
+ )
240
+
241
+ def to_array(self) -> ndarray: # type: ignore
242
+ return np.array([point.to_array() for point in self.points])
243
+
244
+ def to_list(self) -> List[List[float]]:
245
+ return self.to_array().tolist() # type: ignore
246
+
247
+ def to_tuple(self) -> List[List[float]]:
248
+ return tuple(tuple(t) for t in self.to_array().tolist()) # type: ignore
249
+
250
+ def to_face_vertices(self) -> "FaceVertices":
251
+ return FaceVertices(points=self.points)
252
+
253
+ def get_local_coordinate_system(self) -> CoordinateSystem:
254
+ vectors = [
255
+ (a - b).normalize().to_array() for a, b in combinations(self.points, 2)
256
+ ]
257
+ found = False
258
+ for v1, v2, v3 in combinations(vectors, 3):
259
+ if (
260
+ np.isclose(abs(np.dot(v1, v2)), 0)
261
+ and np.isclose(abs(np.dot(v1, v3)), 0)
262
+ and np.isclose(abs(np.dot(v2, v3)), 0)
263
+ ):
264
+ found = True
265
+ x = Vector.from_array(v1)
266
+ y = Vector.from_array(v2)
267
+ z = Vector.from_array(v3)
268
+ break
269
+ if not found:
270
+ raise ValueError("Cannot find coordinate system")
271
+ return CoordinateSystem(x=x, y=y, z=z)
272
+
273
+ def get_bounding_box(self) -> "Vertices":
274
+ coordinates = self.get_local_coordinate_system()
275
+ projected = coordinates.project(self.to_array())
276
+ points_ = open3d.utility.Vector3dVector(projected)
277
+ aab = open3d.geometry.AxisAlignedBoundingBox.create_from_points(points_)
278
+ reversed = coordinates.inverse(np.array(aab.get_box_points()))
279
+ return Vertices.from_arrays(reversed)
280
+
281
+ def is_box_shaped(self) -> bool:
282
+ return len(self.points) == 8
283
+
284
+
285
+ class FaceVertices(Vertices):
286
+
287
+ @model_validator(mode="after")
288
+ def _model_validator(self) -> "FaceVertices":
289
+ if len(self.points) < 3:
290
+ raise ValueError("Face must have more than 3 vertices.")
291
+ return self
292
+
293
+ @computed_field
294
+ def _vector_1(self) -> Vector:
295
+ point_0 = self.points[0]
296
+ point_1 = self.points[1]
297
+ vector_0 = point_1 - point_0
298
+ return Vector.from_array(
299
+ vector_0.to_array() / np.linalg.norm(vector_0.to_array())
300
+ )
301
+
302
+ @computed_field
303
+ def _vector_2(self) -> Vector:
304
+ point_0 = self.points[0]
305
+ point_2 = self.points[2]
306
+ vector_0 = point_2 - point_0
307
+ return Vector.from_array(
308
+ vector_0.to_array() / np.linalg.norm(vector_0.to_array())
309
+ )
310
+
311
+ def get_normal(self) -> Vector:
312
+ normal_vector = self._vector_1 * self._vector_2 # type: ignore
313
+ normal_normalized = normal_vector.to_array() / np.linalg.norm(
314
+ normal_vector.to_array()
315
+ )
316
+ return Vector.from_array(normal_normalized)
317
+
318
+ def get_coordinates(self) -> CoordinateSystem:
319
+ z_axis = self.get_normal()
320
+ x_axis = self._vector_1
321
+ y_axis = z_axis * x_axis # type: ignore
322
+ return CoordinateSystem(x=x_axis, y=y_axis, z=z_axis)
323
+
324
+ def project(self, vertices: "FaceVertices") -> "ProjectedFaceVertices":
325
+ coordinates = self.get_coordinates()
326
+ projected = coordinates.project(vertices.to_array())
327
+ return ProjectedFaceVertices.from_arrays_(projected, coordinates)
328
+
329
+ def get_face_area(self) -> float:
330
+ projected = self.project(self)
331
+ return float(round(projected.to_polygon().area, ROUNDING_FACTOR))
332
+
333
+ def get_center(self) -> Point:
334
+ x = np.mean([point.x for point in self.points])
335
+ y = np.mean([point.y for point in self.points])
336
+ z = np.mean([point.z for point in self.points])
337
+ return Point(x=x, y=y, z=z)
338
+
339
+ def get_distance(self, other: "FaceVertices") -> float:
340
+ return math.dist(self.get_center().to_list(), other.get_center().to_list())
341
+
342
+
343
+ class FixedIndex(BaseModel):
344
+ index: int
345
+ value: float
346
+
347
+
348
+ class ProjectedFaceVertices(FaceVertices):
349
+ coordinate_system: CoordinateSystem
350
+
351
+ def get_fixed_index(self) -> FixedIndex:
352
+ fixed_indexes = [
353
+ FixedIndex(index=i, value=x[0])
354
+ for i, x in enumerate(self.to_array().T)
355
+ if len(set(x)) == 1
356
+ ]
357
+ if len(fixed_indexes) != 1:
358
+ raise ValueError("No or wrong fixed index found")
359
+ return fixed_indexes[0]
360
+
361
+ def to_polygon(self) -> Polygon:
362
+ vertices_ = self.to_list()
363
+ try:
364
+ fixed_index = self.get_fixed_index()
365
+ except ValueError:
366
+ return Polygon()
367
+ indexes = [0, 1, 2]
368
+ indexes.remove(fixed_index.index)
369
+ vertices_ = [*vertices_, vertices_[0]]
370
+ points = [np.array(v)[indexes] for v in vertices_]
371
+ return Polygon(points)
372
+
373
+ def common_vertices(self, polygon: Polygon) -> FaceVertices:
374
+ fixed_index = self.get_fixed_index()
375
+ coords = [list(coord) for coord in list(polygon.exterior.coords)]
376
+ [coord.insert(fixed_index.index, fixed_index.value) for coord in coords] # type: ignore
377
+ vertices = FaceVertices.from_arrays(np.array(coords))
378
+ original = self.coordinate_system.inverse(vertices.to_array())
379
+ return FaceVertices.from_arrays(original) # type: ignore
380
+
381
+ @classmethod
382
+ def from_arrays_(
383
+ cls, arrays: ndarray[Any, Any], coordinate_system: CoordinateSystem
384
+ ) -> "ProjectedFaceVertices":
385
+ return cls(
386
+ points=[Point(x=array[0], y=array[1], z=array[2]) for array in arrays],
387
+ coordinate_system=coordinate_system,
388
+ )
389
+
390
+
391
+ class CommonSurface(BaseShow):
392
+ area: float
393
+ orientation: Vector
394
+ main_vertices: FaceVertices
395
+ common_vertices: FaceVertices
396
+ exterior: bool = True
397
+
398
+ def __hash__(self) -> int:
399
+ return hash(
400
+ (
401
+ self.area,
402
+ tuple(self.orientation.to_list()),
403
+ self.main_vertices.to_tuple(),
404
+ self.common_vertices.to_tuple(),
405
+ )
406
+ )
407
+
408
+ @model_validator(mode="after")
409
+ def _model_validator(self) -> "CommonSurface":
410
+ self.area = round(self.area, ROUNDING_FACTOR)
411
+ return self
412
+
413
+ def description(self) -> tuple[list[float], list[float]]:
414
+ return ([self.area], self.orientation.to_list())
415
+
416
+ def lines(self) -> List[Line]:
417
+ lines = []
418
+ lst = self.common_vertices.to_list()[:4]
419
+
420
+ # for a, b in [[lst[i], lst[(i + 1) % len(lst)]] for i in range(len(lst))]:
421
+ color = "red" if self.exterior else "blue"
422
+ alpha = 0.1 if self.exterior else 0.9
423
+ lines.append(Mesh([lst, [(0, 1, 2, 3)]], c=color, alpha=alpha))
424
+ arrow = Arrow(
425
+ self.main_vertices.get_center().to_list(),
426
+ (
427
+ self.main_vertices.get_center().to_array() + self.orientation.to_array()
428
+ ).tolist(),
429
+ c="deepskyblue",
430
+ s=0.001, # thinner shaft
431
+ head_length=0.05, # smaller tip
432
+ head_radius=0.05, # sharper tip
433
+ res=16, # shaft resolution
434
+ )
435
+ lines.append(arrow)
436
+ return lines
437
+
438
+
439
+ Libraries = Literal["IDEAS", "Buildings", "reduced_order", "iso_13790"]
@@ -0,0 +1,197 @@
1
+ from itertools import combinations
2
+ from logging import getLogger
3
+ from typing import List, Optional, Any, Tuple
4
+
5
+ import ifcopenshell
6
+ import ifcopenshell.geom
7
+ import ifcopenshell.util.placement
8
+ import ifcopenshell.util.shape
9
+ import numpy as np
10
+ import open3d # type: ignore
11
+ from ifcopenshell import entity_instance
12
+ from pydantic import (
13
+ BaseModel,
14
+ Field,
15
+ ConfigDict,
16
+ )
17
+ from scipy.spatial import ConvexHull # type: ignore
18
+ from vedo import Line # type: ignore
19
+
20
+ from ifctrano.base import (
21
+ Point,
22
+ Vector,
23
+ Vertices,
24
+ BaseModelConfig,
25
+ settings,
26
+ CommonSurface,
27
+ AREA_TOLERANCE,
28
+ FaceVertices,
29
+ BaseShow,
30
+ )
31
+ from ifctrano.exceptions import VectorWithNansError
32
+
33
+ logger = getLogger(__name__)
34
+
35
+
36
+ class BoundingBoxFace(BaseModelConfig):
37
+ vertices: FaceVertices
38
+ normal: Vector
39
+
40
+ @classmethod
41
+ def build(cls, vertices: Vertices) -> "BoundingBoxFace":
42
+ face_vertices = vertices.to_face_vertices()
43
+
44
+ return cls(vertices=face_vertices, normal=face_vertices.get_normal())
45
+
46
+
47
+ class BoundingBoxFaces(BaseModel):
48
+ faces: List[BoundingBoxFace]
49
+
50
+ def description(self) -> List[tuple[Any, Tuple[float, float, float]]]:
51
+ return sorted([(f.vertices.to_list(), f.normal.to_tuple()) for f in self.faces])
52
+
53
+ @classmethod
54
+ def build(
55
+ cls, box_points: np.ndarray[tuple[int, ...], np.dtype[np.float64]]
56
+ ) -> "BoundingBoxFaces":
57
+ faces = [
58
+ [0, 1, 6, 3],
59
+ [2, 5, 4, 7],
60
+ [0, 3, 5, 2],
61
+ [1, 7, 4, 6],
62
+ [0, 2, 7, 1],
63
+ [3, 6, 4, 5],
64
+ ]
65
+ faces_ = [
66
+ BoundingBoxFace.build(Vertices.from_arrays(np.array(box_points)[face]))
67
+ for face in faces
68
+ ]
69
+ return cls(faces=faces_)
70
+
71
+
72
+ class ExtendCommonSurface(CommonSurface):
73
+ distance: float
74
+
75
+ def to_common_surface(self) -> CommonSurface:
76
+ return CommonSurface(
77
+ area=self.area,
78
+ orientation=self.orientation,
79
+ main_vertices=self.main_vertices,
80
+ common_vertices=self.common_vertices,
81
+ )
82
+
83
+
84
+ class OrientedBoundingBox(BaseShow):
85
+ model_config = ConfigDict(arbitrary_types_allowed=True)
86
+ faces: BoundingBoxFaces
87
+ centroid: Point
88
+ area_tolerance: float = Field(default=AREA_TOLERANCE)
89
+ volume: float
90
+ height: float
91
+ entity: Optional[entity_instance] = None
92
+
93
+ def lines(self) -> List[Line]:
94
+ lines = []
95
+ for f in self.faces.faces:
96
+ face = f.vertices.to_list()
97
+ for a, b in combinations(face, 2):
98
+ lines.append(Line(a, b))
99
+ return lines
100
+
101
+ def intersect_faces(self, other: "OrientedBoundingBox") -> Optional[CommonSurface]:
102
+ extend_surfaces = []
103
+ for face in self.faces.faces:
104
+
105
+ for other_face in other.faces.faces:
106
+ vector = face.normal * other_face.normal
107
+ if vector.is_null():
108
+ projected_face_1 = face.vertices.project(face.vertices)
109
+ projected_face_2 = face.vertices.project(other_face.vertices)
110
+ polygon_1 = projected_face_1.to_polygon()
111
+ polygon_2 = projected_face_2.to_polygon()
112
+ intersection = polygon_2.intersection(polygon_1)
113
+ if intersection.area > self.area_tolerance:
114
+ distance = projected_face_1.get_distance(projected_face_2)
115
+ area = intersection.area
116
+ try:
117
+ direction_vector = (
118
+ other.centroid - self.centroid
119
+ ).normalize()
120
+ orientation = direction_vector.project(
121
+ face.normal
122
+ ).normalize()
123
+ except VectorWithNansError as e:
124
+ logger.warning(
125
+ "Orientation vector was not properly computed when computing the intersection between "
126
+ f"two elements "
127
+ f"({(self.entity.GlobalId, self.entity.is_a(), self.entity.Name) if self.entity else None}" # noqa: E501
128
+ f", {(other.entity.GlobalId, other.entity.is_a(), other.entity.Name)if other.entity else None}). Error: {e}" # noqa: E501
129
+ )
130
+ continue
131
+ extend_surfaces.append(
132
+ ExtendCommonSurface(
133
+ distance=distance,
134
+ area=area,
135
+ orientation=orientation,
136
+ main_vertices=face.vertices,
137
+ common_vertices=projected_face_1.common_vertices(
138
+ intersection
139
+ ),
140
+ )
141
+ )
142
+
143
+ if extend_surfaces:
144
+ if not all(
145
+ e.orientation == extend_surfaces[0].orientation for e in extend_surfaces
146
+ ):
147
+ logger.warning("Different orientations found. taking the max area")
148
+ max_area = max([e.area for e in extend_surfaces])
149
+ extend_surfaces = [e for e in extend_surfaces if e.area == max_area]
150
+ extend_surface = sorted(
151
+ extend_surfaces, key=lambda x: x.distance, reverse=True
152
+ )[-1]
153
+ return extend_surface.to_common_surface()
154
+ else:
155
+ logger.warning(
156
+ "No common surfaces found between between "
157
+ f"two elements "
158
+ f"({(self.entity.GlobalId, self.entity.is_a(), self.entity.Name) if self.entity else None}, "
159
+ f"{(other.entity.GlobalId, other.entity.is_a(), other.entity.Name) if other.entity else None})."
160
+ )
161
+ return None
162
+
163
+ @classmethod
164
+ def from_vertices(
165
+ cls,
166
+ vertices: np.ndarray[tuple[int, ...], np.dtype[np.float64]],
167
+ entity: Optional[entity_instance] = None,
168
+ ) -> "OrientedBoundingBox":
169
+ points_ = open3d.utility.Vector3dVector(vertices)
170
+ mobb = open3d.geometry.OrientedBoundingBox.create_from_points_minimal(
171
+ points_, robust=True
172
+ )
173
+ height = (mobb.get_max_bound() - mobb.get_min_bound())[
174
+ 2
175
+ ] # assuming that height is the z axis
176
+ centroid = Point.from_array(mobb.get_center())
177
+ faces = BoundingBoxFaces.build(np.array(mobb.get_box_points()))
178
+ return cls(
179
+ faces=faces,
180
+ centroid=centroid,
181
+ volume=mobb.volume(),
182
+ height=height,
183
+ entity=entity,
184
+ )
185
+
186
+ @classmethod
187
+ def from_entity(cls, entity: entity_instance) -> "OrientedBoundingBox":
188
+ entity_shape = ifcopenshell.geom.create_shape(settings, entity)
189
+ vertices = ifcopenshell.util.shape.get_shape_vertices(
190
+ entity_shape, entity_shape.geometry # type: ignore
191
+ )
192
+ vertices_ = Vertices.from_arrays(np.asarray(vertices))
193
+ hull = ConvexHull(vertices_.to_array())
194
+ vertices_ = Vertices.from_arrays(vertices_.to_array()[hull.vertices])
195
+ points_ = open3d.utility.Vector3dVector(vertices_.to_array())
196
+ aab = open3d.geometry.AxisAlignedBoundingBox.create_from_points(points_)
197
+ return cls.from_vertices(aab.get_box_points(), entity)