ifctrano 0.1.12__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ifctrano/__init__.py +3 -0
- ifctrano/base.py +266 -13
- ifctrano/bounding_box.py +99 -169
- ifctrano/building.py +109 -54
- ifctrano/construction.py +227 -0
- ifctrano/example/verification.ifc +3 -3043
- ifctrano/exceptions.py +4 -0
- ifctrano/main.py +55 -2
- ifctrano/space_boundary.py +168 -98
- ifctrano/types.py +5 -0
- ifctrano/utils.py +29 -0
- {ifctrano-0.1.12.dist-info → ifctrano-0.3.0.dist-info}/METADATA +6 -5
- ifctrano-0.3.0.dist-info/RECORD +16 -0
- {ifctrano-0.1.12.dist-info → ifctrano-0.3.0.dist-info}/WHEEL +1 -1
- ifctrano-0.1.12.dist-info/RECORD +0 -13
- {ifctrano-0.1.12.dist-info → ifctrano-0.3.0.dist-info}/LICENSE +0 -0
- {ifctrano-0.1.12.dist-info → ifctrano-0.3.0.dist-info}/entry_points.txt +0 -0
ifctrano/__init__.py
CHANGED
ifctrano/base.py
CHANGED
@@ -1,9 +1,23 @@
|
|
1
|
-
|
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
|
2
7
|
|
3
8
|
import ifcopenshell.geom
|
4
9
|
import numpy as np
|
10
|
+
import open3d # type: ignore
|
5
11
|
from numpy import ndarray
|
6
|
-
from pydantic import
|
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
|
7
21
|
|
8
22
|
from ifctrano.exceptions import VectorWithNansError
|
9
23
|
|
@@ -22,6 +36,48 @@ def round_two_decimals(value: float) -> float:
|
|
22
36
|
return round(value, 10)
|
23
37
|
|
24
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
|
+
|
25
81
|
class BasePoint(BaseModel):
|
26
82
|
x: Annotated[float, BeforeValidator(round_two_decimals)]
|
27
83
|
y: Annotated[float, BeforeValidator(round_two_decimals)]
|
@@ -92,12 +148,15 @@ class Vector(BasePoint):
|
|
92
148
|
a = self.dot(other) / other.dot(other)
|
93
149
|
return Vector(x=a * other.x, y=a * other.y, z=a * other.z)
|
94
150
|
|
95
|
-
def
|
151
|
+
def normalize(self) -> "Vector":
|
96
152
|
normalized_vector = self.to_array() / np.linalg.norm(self.to_array())
|
97
153
|
return Vector(
|
98
154
|
x=normalized_vector[0], y=normalized_vector[1], z=normalized_vector[2]
|
99
155
|
)
|
100
156
|
|
157
|
+
def norm(self) -> float:
|
158
|
+
return float(np.linalg.norm(self.to_array()))
|
159
|
+
|
101
160
|
def to_array(self) -> np.ndarray: # type: ignore
|
102
161
|
return np.array([self.x, self.y, self.z])
|
103
162
|
|
@@ -108,8 +167,8 @@ class Vector(BasePoint):
|
|
108
167
|
normal_index_list = [abs(v) for v in self.to_list()]
|
109
168
|
return normal_index_list.index(max(normal_index_list))
|
110
169
|
|
111
|
-
def
|
112
|
-
return all(abs(value) <
|
170
|
+
def is_null(self, tolerance: float = 0.1) -> bool:
|
171
|
+
return all(abs(value) < tolerance for value in self.to_list())
|
113
172
|
|
114
173
|
@classmethod
|
115
174
|
def from_array(cls, array: np.ndarray) -> "Vector": # type: ignore
|
@@ -141,6 +200,15 @@ class CoordinateSystem(BaseModel):
|
|
141
200
|
y: Vector
|
142
201
|
z: Vector
|
143
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
|
+
|
144
212
|
@classmethod
|
145
213
|
def from_array(cls, array: np.ndarray) -> "CoordinateSystem": # type: ignore
|
146
214
|
return cls(
|
@@ -152,18 +220,20 @@ class CoordinateSystem(BaseModel):
|
|
152
220
|
def to_array(self) -> np.ndarray: # type: ignore
|
153
221
|
return np.array([self.x.to_array(), self.y.to_array(), self.z.to_array()])
|
154
222
|
|
155
|
-
def
|
156
|
-
return np.dot(array, self.to_array()) # type: ignore
|
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
|
157
225
|
|
158
|
-
def
|
159
|
-
return np.dot(array, np.linalg.inv(self.to_array())) # type: ignore
|
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
|
160
228
|
|
161
229
|
|
162
230
|
class Vertices(BaseModel):
|
163
231
|
points: List[Point]
|
164
232
|
|
165
233
|
@classmethod
|
166
|
-
def from_arrays(
|
234
|
+
def from_arrays(
|
235
|
+
cls, arrays: np.ndarray[tuple[int, ...], np.dtype[np.float64]]
|
236
|
+
) -> "Vertices":
|
167
237
|
return cls(
|
168
238
|
points=[Point(x=array[0], y=array[1], z=array[2]) for array in arrays]
|
169
239
|
)
|
@@ -174,13 +244,196 @@ class Vertices(BaseModel):
|
|
174
244
|
def to_list(self) -> List[List[float]]:
|
175
245
|
return self.to_array().tolist() # type: ignore
|
176
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):
|
177
286
|
|
178
|
-
|
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):
|
179
392
|
area: float
|
180
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
|
181
412
|
|
182
|
-
def description(self) ->
|
183
|
-
return self.area, self.orientation.to_list()
|
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
|
184
437
|
|
185
438
|
|
186
439
|
Libraries = Literal["IDEAS", "Buildings", "reduced_order", "iso_13790"]
|