ifctrano 0.1.12__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 +3 -0
- ifctrano/base.py +256 -12
- ifctrano/bounding_box.py +97 -168
- ifctrano/building.py +104 -52
- ifctrano/construction.py +227 -0
- ifctrano/example/verification.ifc +3 -3043
- ifctrano/main.py +55 -2
- ifctrano/space_boundary.py +100 -98
- ifctrano/types.py +5 -0
- ifctrano/utils.py +29 -0
- {ifctrano-0.1.12.dist-info → ifctrano-0.2.0.dist-info}/METADATA +6 -5
- ifctrano-0.2.0.dist-info/RECORD +16 -0
- {ifctrano-0.1.12.dist-info → ifctrano-0.2.0.dist-info}/WHEEL +1 -1
- ifctrano-0.1.12.dist-info/RECORD +0 -13
- {ifctrano-0.1.12.dist-info → ifctrano-0.2.0.dist-info}/LICENSE +0 -0
- {ifctrano-0.1.12.dist-info → ifctrano-0.2.0.dist-info}/entry_points.txt +0 -0
ifctrano/__init__.py
CHANGED
ifctrano/base.py
CHANGED
@@ -1,11 +1,24 @@
|
|
1
|
-
|
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
|
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) <
|
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
|
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
|
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(
|
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
|
-
|
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
|
183
|
-
return
|
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
|
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
|
-
|
28
|
+
FaceVertices,
|
29
|
+
BaseShow,
|
27
30
|
)
|
28
|
-
from ifctrano.exceptions import
|
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:
|
37
|
+
vertices: FaceVertices
|
58
38
|
normal: Vector
|
59
|
-
coordinate_system: CoordinateSystem
|
60
39
|
|
61
40
|
@classmethod
|
62
|
-
def build(
|
63
|
-
|
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
|
-
|
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,
|
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
|
-
|
145
|
-
|
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=
|
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(
|
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(
|
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
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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.
|
182
|
-
"Orientation vector was not properly computed when computing the intersection between"
|
183
|
-
f"two elements
|
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,
|
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,
|
168
|
+
cls,
|
169
|
+
vertices: np.ndarray[tuple[int, ...], np.dtype[np.float64]],
|
170
|
+
entity: Optional[entity_instance] = None,
|
207
171
|
) -> "OrientedBoundingBox":
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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=
|
256
|
-
volume=
|
257
|
-
height=
|
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
|
-
|
193
|
+
vertices_ = Vertices.from_arrays(np.asarray(vertices))
|
194
|
+
|
195
|
+
vertices_ = vertices_.get_bounding_box()
|
196
|
+
return cls.from_vertices(vertices_.to_array(), entity)
|