ifctrano 0.1.11__py3-none-any.whl → 0.2.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.
ifctrano/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ import warnings
2
+
3
+ warnings.filterwarnings("ignore", category=RuntimeWarning)
ifctrano/base.py CHANGED
@@ -1,11 +1,24 @@
1
- from typing import Tuple, Literal, List, Annotated, Sequence
1
+ import json
2
+ import math
3
+ from pathlib import Path
4
+ from typing import Tuple, Literal, List, Annotated, Any, Dict, cast
2
5
 
3
6
  import ifcopenshell.geom
4
7
  import numpy as np
8
+ import open3d # type: ignore
5
9
  from numpy import ndarray
6
- from pydantic import BaseModel, BeforeValidator, ConfigDict, model_validator
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
7
19
 
8
20
  from ifctrano.exceptions import VectorWithNansError
21
+ from multiprocessing import Process
9
22
 
10
23
  settings = ifcopenshell.geom.settings() # type: ignore
11
24
  Coordinate = Literal["x", "y", "z"]
@@ -22,6 +35,48 @@ def round_two_decimals(value: float) -> float:
22
35
  return round(value, 10)
23
36
 
24
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
+
25
80
  class BasePoint(BaseModel):
26
81
  x: Annotated[float, BeforeValidator(round_two_decimals)]
27
82
  y: Annotated[float, BeforeValidator(round_two_decimals)]
@@ -108,8 +163,8 @@ class Vector(BasePoint):
108
163
  normal_index_list = [abs(v) for v in self.to_list()]
109
164
  return normal_index_list.index(max(normal_index_list))
110
165
 
111
- def is_a_zero(self) -> bool:
112
- return all(abs(value) < 0.1 for value in self.to_list())
166
+ def is_a_zero(self, tolerance: float = 0.1) -> bool:
167
+ return all(abs(value) < tolerance for value in self.to_list())
113
168
 
114
169
  @classmethod
115
170
  def from_array(cls, array: np.ndarray) -> "Vector": # type: ignore
@@ -141,6 +196,15 @@ class CoordinateSystem(BaseModel):
141
196
  y: Vector
142
197
  z: Vector
143
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
+
144
208
  @classmethod
145
209
  def from_array(cls, array: np.ndarray) -> "CoordinateSystem": # type: ignore
146
210
  return cls(
@@ -152,18 +216,20 @@ class CoordinateSystem(BaseModel):
152
216
  def to_array(self) -> np.ndarray: # type: ignore
153
217
  return np.array([self.x.to_array(), self.y.to_array(), self.z.to_array()])
154
218
 
155
- def project(self, array: np.array) -> np.array: # type: ignore
156
- return np.dot(array, self.to_array()) # type: ignore
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
157
221
 
158
- def inverse(self, array: np.array) -> np.ndarray: # type: ignore
159
- return np.dot(array, np.linalg.inv(self.to_array())) # type: ignore
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
160
224
 
161
225
 
162
226
  class Vertices(BaseModel):
163
227
  points: List[Point]
164
228
 
165
229
  @classmethod
166
- def from_arrays(cls, arrays: Sequence[np.ndarray[np.float64]]) -> "Vertices": # type: ignore
230
+ def from_arrays(
231
+ cls, arrays: np.ndarray[tuple[int, ...], np.dtype[np.float64]]
232
+ ) -> "Vertices":
167
233
  return cls(
168
234
  points=[Point(x=array[0], y=array[1], z=array[2]) for array in arrays]
169
235
  )
@@ -174,13 +240,191 @@ class Vertices(BaseModel):
174
240
  def to_list(self) -> List[List[float]]:
175
241
  return self.to_array().tolist() # type: ignore
176
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
+ )
177
292
 
178
- class CommonSurface(BaseModel):
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):
179
383
  area: float
180
384
  orientation: Vector
385
+ main_vertices: FaceVertices
386
+ common_vertices: FaceVertices
387
+ exterior: bool = True
181
388
 
182
- def description(self) -> Tuple[float, List[float]]:
183
- return self.area, self.orientation.to_list()
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
184
428
 
185
429
 
186
430
  Libraries = Literal["IDEAS", "Buildings", "reduced_order", "iso_13790"]
ifctrano/bounding_box.py CHANGED
@@ -1,105 +1,47 @@
1
+ from itertools import combinations
1
2
  from logging import getLogger
2
- from typing import List, Optional, Any, Tuple, cast
3
+ from typing import List, Optional, Any, Tuple
3
4
 
4
5
  import ifcopenshell
5
- import numpy as np
6
- from ifcopenshell import entity_instance
7
6
  import ifcopenshell.geom
7
+ import ifcopenshell.util.placement
8
8
  import ifcopenshell.util.shape
9
+ import numpy as np
10
+ import open3d # type: ignore
11
+ from ifcopenshell import entity_instance
9
12
  from pydantic import (
10
13
  BaseModel,
11
14
  Field,
15
+ ConfigDict,
12
16
  )
13
17
  from shapely import Polygon # type: ignore
18
+ from vedo import Line # type: ignore
14
19
 
15
20
  from ifctrano.base import (
16
21
  Point,
17
22
  Vector,
18
- P,
19
- Sign,
20
- CoordinateSystem,
21
23
  Vertices,
22
24
  BaseModelConfig,
23
25
  settings,
24
26
  CommonSurface,
25
27
  AREA_TOLERANCE,
26
- ROUNDING_FACTOR,
28
+ FaceVertices,
29
+ BaseShow,
27
30
  )
28
- from ifctrano.exceptions import BoundingBoxFaceError, VectorWithNansError
31
+ from ifctrano.exceptions import VectorWithNansError
29
32
 
30
33
  logger = getLogger(__name__)
31
34
 
32
35
 
33
- def get_normal(
34
- centroid: Point,
35
- difference: Point,
36
- face_signs: List[Sign],
37
- coordinate_system: CoordinateSystem,
38
- ) -> Vector:
39
- point_0 = centroid + difference.s(face_signs[0])
40
- point_1 = centroid + difference.s(face_signs[1])
41
- point_2 = centroid + difference.s(face_signs[2])
42
- vector_1 = coordinate_system.project((point_1 - point_0).to_array())
43
- vector_2 = coordinate_system.project((point_2 - point_0).to_array())
44
- array = (
45
- (Vector.from_array(vector_1) * Vector.from_array(vector_2)).norm().to_array()
46
- )
47
- return Vector.from_array(array)
48
-
49
-
50
- class Polygon2D(BaseModelConfig):
51
- polygon: Polygon
52
- normal: Vector
53
- length: float
54
-
55
-
56
36
  class BoundingBoxFace(BaseModelConfig):
57
- vertices: Vertices
37
+ vertices: FaceVertices
58
38
  normal: Vector
59
- coordinate_system: CoordinateSystem
60
39
 
61
40
  @classmethod
62
- def build(
63
- cls,
64
- centroid: Point,
65
- difference: Point,
66
- face_signs: List[Sign],
67
- coordinate_system: CoordinateSystem,
68
- ) -> "BoundingBoxFace":
69
- if len(face_signs) != len(set(face_signs)):
70
- raise BoundingBoxFaceError("Face signs must be unique")
71
- normal = get_normal(centroid, difference, face_signs, coordinate_system)
72
- vertices_ = [(centroid + difference.s(s)).to_list() for s in face_signs]
73
- vertices_ = [*vertices_, vertices_[0]]
74
- vertices__ = [coordinate_system.project(v) for v in vertices_]
75
- vertices = Vertices.from_arrays(vertices__)
76
-
77
- return cls(
78
- vertices=vertices, normal=normal, coordinate_system=coordinate_system
79
- )
80
-
81
- def get_face_area(self) -> float:
82
- polygon_2d = self.get_2d_polygon(self.coordinate_system)
83
- return cast(float, round(polygon_2d.polygon.area, ROUNDING_FACTOR))
41
+ def build(cls, vertices: Vertices) -> "BoundingBoxFace":
42
+ face_vertices = vertices.to_face_vertices()
84
43
 
85
- def get_2d_polygon(self, coordinate_system: CoordinateSystem) -> Polygon2D:
86
-
87
- projected_vertices = coordinate_system.inverse(self.vertices.to_array())
88
- projected_normal_index = Vector.from_array(
89
- coordinate_system.inverse(self.normal.to_array())
90
- ).get_normal_index()
91
- polygon = Polygon(
92
- [
93
- [v_ for i, v_ in enumerate(v) if i != projected_normal_index]
94
- for v in projected_vertices.tolist()
95
- ]
96
- )
97
-
98
- return Polygon2D(
99
- polygon=polygon,
100
- normal=self.normal,
101
- length=projected_vertices.tolist()[0][projected_normal_index],
102
- )
44
+ return cls(vertices=face_vertices, normal=face_vertices.get_normal())
103
45
 
104
46
 
105
47
  class BoundingBoxFaces(BaseModel):
@@ -110,56 +52,56 @@ class BoundingBoxFaces(BaseModel):
110
52
 
111
53
  @classmethod
112
54
  def build(
113
- cls, centroid: Point, difference: Point, coordinate_system: CoordinateSystem
55
+ cls, box_points: np.ndarray[tuple[int, ...], np.dtype[np.float64]]
114
56
  ) -> "BoundingBoxFaces":
115
- face_signs = [
116
- [Sign(x=-1, y=-1, z=-1), Sign(y=-1, z=-1), Sign(z=-1), Sign(x=-1, z=-1)],
117
- [Sign(x=-1, y=-1), Sign(y=-1), Sign(), Sign(x=-1)],
118
- [
119
- Sign(x=-1, y=-1, z=-1),
120
- Sign(x=-1, y=1, z=-1),
121
- Sign(x=-1, y=1, z=1),
122
- Sign(x=-1, y=-1, z=1),
123
- ],
124
- [
125
- Sign(x=1, y=-1, z=-1),
126
- Sign(x=1, y=1, z=-1),
127
- Sign(x=1, y=1, z=1),
128
- Sign(x=1, y=-1, z=1),
129
- ],
130
- [
131
- Sign(x=-1, y=-1, z=-1),
132
- Sign(x=1, y=-1, z=-1),
133
- Sign(x=1, y=-1, z=1),
134
- Sign(x=-1, y=-1, z=1),
135
- ],
136
- [
137
- Sign(x=-1, y=1, z=-1),
138
- Sign(x=1, y=1, z=-1),
139
- Sign(x=1, y=1, z=1),
140
- Sign(x=-1, y=1, z=1),
141
- ],
142
- ]
143
57
  faces = [
144
- BoundingBoxFace.build(centroid, difference, face_sign, coordinate_system)
145
- for face_sign in face_signs
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
146
68
  ]
147
- return cls(faces=faces)
69
+ return cls(faces=faces_)
148
70
 
149
71
 
150
72
  class ExtendCommonSurface(CommonSurface):
151
73
  distance: float
152
74
 
153
75
  def to_common_surface(self) -> CommonSurface:
154
- return CommonSurface(area=self.area, orientation=self.orientation)
76
+ return CommonSurface(
77
+ area=self.area,
78
+ orientation=self.orientation,
79
+ main_vertices=self.main_vertices,
80
+ common_vertices=self.common_vertices,
81
+ )
155
82
 
156
83
 
157
- class OrientedBoundingBox(BaseModel):
84
+ class OrientedBoundingBox(BaseShow):
85
+ model_config = ConfigDict(arbitrary_types_allowed=True)
158
86
  faces: BoundingBoxFaces
159
87
  centroid: Point
160
88
  area_tolerance: float = Field(default=AREA_TOLERANCE)
161
89
  volume: float
162
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)
163
105
 
164
106
  def intersect_faces(self, other: "OrientedBoundingBox") -> Optional[CommonSurface]:
165
107
  extend_surfaces = []
@@ -168,26 +110,39 @@ class OrientedBoundingBox(BaseModel):
168
110
  for other_face in other.faces.faces:
169
111
  vector = face.normal * other_face.normal
170
112
  if vector.is_a_zero():
171
- polygon_1 = other_face.get_2d_polygon(face.coordinate_system)
172
- polygon_2 = face.get_2d_polygon(face.coordinate_system)
173
- intersection = polygon_2.polygon.intersection(polygon_1.polygon)
174
- if intersection.area > self.area_tolerance:
175
- distance = abs(polygon_1.length - polygon_2.length)
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)
176
122
  area = intersection.area
177
- direction_vector = (other.centroid - self.centroid).norm()
178
123
  try:
124
+ direction_vector = (other.centroid - self.centroid).norm()
179
125
  orientation = direction_vector.project(face.normal).norm()
180
126
  except VectorWithNansError as e:
181
- logger.error(
182
- "Orientation vector was not properly computed when computing the intersection between"
183
- f"two elements. Error: {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
184
132
  )
185
133
  continue
186
134
  extend_surfaces.append(
187
135
  ExtendCommonSurface(
188
- distance=distance, area=area, orientation=orientation
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
+ ),
189
143
  )
190
144
  )
145
+
191
146
  if extend_surfaces:
192
147
  if not all(
193
148
  e.orientation == extend_surfaces[0].orientation for e in extend_surfaces
@@ -199,69 +154,43 @@ class OrientedBoundingBox(BaseModel):
199
154
  extend_surfaces, key=lambda x: x.distance, reverse=True
200
155
  )[-1]
201
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
+ )
202
164
  return None
203
165
 
204
166
  @classmethod
205
167
  def from_vertices(
206
- cls, vertices: np.ndarray[tuple[int, ...], np.dtype[np.float64]]
168
+ cls,
169
+ vertices: np.ndarray[tuple[int, ...], np.dtype[np.float64]],
170
+ entity: Optional[entity_instance] = None,
207
171
  ) -> "OrientedBoundingBox":
208
- vertices_np = np.array(vertices)
209
- points = np.asarray(vertices_np)
210
- cov = np.cov(points, y=None, rowvar=0, bias=0) # type: ignore
211
- v, vect = np.linalg.eig(np.round(cov, ROUNDING_FACTOR))
212
- tvect = np.transpose(vect)
213
- points_r = np.dot(points, np.linalg.inv(tvect))
214
-
215
- co_min = np.min(points_r, axis=0)
216
- co_max = np.max(points_r, axis=0)
217
-
218
- xmin, xmax = co_min[0], co_max[0]
219
- ymin, ymax = co_min[1], co_max[1]
220
- zmin, zmax = co_min[2], co_max[2]
221
-
222
- x_len = xmax - xmin
223
- y_len = ymax - ymin
224
- z_len = zmax - zmin
225
- xdif = x_len * 0.5
226
- ydif = y_len * 0.5
227
- zdif = z_len * 0.5
228
-
229
- cx = xmin + xdif
230
- cy = ymin + ydif
231
- cz = zmin + zdif
232
- corners = np.array(
233
- [
234
- [cx - xdif, cy - ydif, cz - zdif],
235
- [cx - xdif, cy + ydif, cz - zdif],
236
- [cx - xdif, cy + ydif, cz + zdif],
237
- [cx - xdif, cy - ydif, cz + zdif],
238
- [cx + xdif, cy + ydif, cz + zdif],
239
- [cx + xdif, cy + ydif, cz - zdif],
240
- [cx + xdif, cy - ydif, cz + zdif],
241
- [cx + xdif, cy - ydif, cz - zdif],
242
- ]
243
- )
244
- corners_ = np.dot(corners, tvect)
245
- dims = np.transpose(corners_)
246
- x_size = np.max(dims[0]) - np.min(dims[0])
247
- y_size = np.max(dims[1]) - np.min(dims[1])
248
- z_size = np.max(dims[2]) - np.min(dims[2])
249
- coordinate_system = CoordinateSystem.from_array(tvect)
250
- c = P(x=cx, y=cy, z=cz)
251
- d = P(x=xdif, y=ydif, z=zdif)
252
- faces = BoundingBoxFaces.build(c, d, coordinate_system)
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()))
253
179
  return cls(
254
180
  faces=faces,
255
- centroid=Point.from_array(coordinate_system.project(c.to_array())),
256
- volume=x_size * y_size * z_size,
257
- height=z_size,
181
+ centroid=centroid,
182
+ volume=mobb.volume(),
183
+ height=height,
184
+ entity=entity,
258
185
  )
259
186
 
260
187
  @classmethod
261
188
  def from_entity(cls, entity: entity_instance) -> "OrientedBoundingBox":
262
189
  entity_shape = ifcopenshell.geom.create_shape(settings, entity)
263
-
264
190
  vertices = ifcopenshell.util.shape.get_shape_vertices(
265
191
  entity_shape, entity_shape.geometry # type: ignore
266
192
  )
267
- return cls.from_vertices(vertices)
193
+ vertices_ = Vertices.from_arrays(np.asarray(vertices))
194
+
195
+ vertices_ = vertices_.get_bounding_box()
196
+ return cls.from_vertices(vertices_.to_array(), entity)