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 CHANGED
@@ -0,0 +1,3 @@
1
+ import warnings
2
+
3
+ warnings.filterwarnings("ignore", category=RuntimeWarning)
ifctrano/base.py CHANGED
@@ -1,9 +1,23 @@
1
- from typing import Tuple, Literal, List, Annotated, Sequence
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 BaseModel, BeforeValidator, ConfigDict, model_validator
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 norm(self) -> "Vector":
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 is_a_zero(self) -> bool:
112
- return all(abs(value) < 0.1 for value in self.to_list())
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 project(self, array: np.array) -> np.array: # type: ignore
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 inverse(self, array: np.array) -> np.ndarray: # type: ignore
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(cls, arrays: Sequence[np.ndarray[np.float64]]) -> "Vertices": # type: ignore
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
- class CommonSurface(BaseModel):
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) -> Tuple[float, List[float]]:
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"]