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