Coverage for bim2sim/utilities/pyocc_tools.py: 50%
335 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1"""
2Common tools for handling OCC Shapes within the bim2sim project.
3"""
4from typing import List, Tuple, Union
6import numpy as np
7from OCC.Core.BRep import BRep_Tool
8from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
9from OCC.Core.BRepBndLib import brepbndlib_Add
10from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, \
11 BRepBuilderAPI_Transform, BRepBuilderAPI_MakePolygon, \
12 BRepBuilderAPI_MakeShell, BRepBuilderAPI_MakeSolid, BRepBuilderAPI_Sewing
13from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier
14from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
15from OCC.Core.BRepGProp import brepgprop_SurfaceProperties, \
16 brepgprop_LinearProperties, brepgprop_VolumeProperties, BRepGProp_Face
17from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
18from OCC.Core.BRepTools import BRepTools_WireExplorer
19from OCC.Core.Bnd import Bnd_Box
20from OCC.Core.Extrema import Extrema_ExtFlag_MIN
21from OCC.Core.GProp import GProp_GProps
22from OCC.Core.Geom import Handle_Geom_Plane_DownCast
23from OCC.Core.ShapeAnalysis import ShapeAnalysis_ShapeContents
24from OCC.Core.ShapeFix import ShapeFix_Face, ShapeFix_Shape
25from OCC.Core.TopAbs import TopAbs_WIRE, TopAbs_FACE, TopAbs_OUT
26from OCC.Core.TopExp import TopExp_Explorer
27from OCC.Core.TopoDS import topods_Wire, TopoDS_Face, TopoDS_Shape, \
28 topods_Face, TopoDS_Edge, TopoDS_Solid, TopoDS_Shell, TopoDS_Builder
29from OCC.Core.gp import gp_XYZ, gp_Pnt, gp_Trsf, gp_Vec
32class PyOCCTools:
33 """Class for Tools handling and modifying Python OCC Shapes"""
35 @staticmethod
36 def remove_coincident_vertices(vert_list: List[gp_Pnt]) -> List[gp_Pnt]:
37 """ remove coincident vertices from list of gp_Pnt.
38 Vertices are coincident if closer than tolerance."""
39 tol_dist = 1e-2
40 new_list = []
41 v_b = np.array(vert_list[-1].Coord())
42 for vert in vert_list:
43 v = np.array(vert.Coord())
44 d_b = np.linalg.norm(v - v_b)
45 if d_b > tol_dist:
46 new_list.append(vert)
47 v_b = v
48 return new_list
50 @staticmethod
51 def remove_collinear_vertices2(vert_list: List[gp_Pnt]) -> List[gp_Pnt]:
52 """ remove collinear vertices from list of gp_Pnt.
53 Vertices are collinear if cross product less tolerance."""
54 tol_cross = 1e-3
55 new_list = []
57 for i, vert in enumerate(vert_list):
58 v = np.array(vert.Coord())
59 v_b = np.array(vert_list[(i - 1) % (len(vert_list))].Coord())
60 v_f = np.array(vert_list[(i + 1) % (len(vert_list))].Coord())
61 v1 = v - v_b
62 v2 = v_f - v_b
63 if np.linalg.norm(np.cross(v1, v2)) / np.linalg.norm(
64 v2) > tol_cross:
65 new_list.append(vert)
66 return new_list
68 @staticmethod
69 def make_faces_from_pnts(
70 pnt_list: Union[List[Tuple[float]], List[gp_Pnt]]) -> TopoDS_Face:
71 """
72 This function returns a TopoDS_Face from list of gp_Pnt
73 :param pnt_list: list of gp_Pnt or Coordinate-Tuples
74 :return: TopoDS_Face
75 """
76 if isinstance(pnt_list[0], tuple):
77 new_list = []
78 for pnt in pnt_list:
79 new_list.append(gp_Pnt(gp_XYZ(pnt[0], pnt[1], pnt[2])))
80 pnt_list = new_list
81 poly = BRepBuilderAPI_MakePolygon()
82 for coord in pnt_list:
83 poly.Add(coord)
84 poly.Close()
85 a_wire = poly.Wire()
86 a_face = BRepBuilderAPI_MakeFace(a_wire).Face()
87 return a_face
89 @staticmethod
90 def get_number_of_vertices(shape: TopoDS_Shape) -> int:
91 """ get number of vertices of a shape"""
92 shape_analysis = ShapeAnalysis_ShapeContents()
93 shape_analysis.Perform(shape)
94 nb_vertex = shape_analysis.NbVertices()
96 return nb_vertex
98 @staticmethod
99 def get_points_of_face(shape: TopoDS_Shape) -> List[gp_Pnt]:
100 """
101 This function returns a list of gp_Pnt of a Surface
102 :param shape: TopoDS_Shape (Surface)
103 :return: pnt_list (list of gp_Pnt)
104 """
105 an_exp = TopExp_Explorer(shape, TopAbs_WIRE)
106 pnt_list = []
107 while an_exp.More():
108 wire = topods_Wire(an_exp.Current())
109 w_exp = BRepTools_WireExplorer(wire)
110 while w_exp.More():
111 pnt1 = BRep_Tool.Pnt(w_exp.CurrentVertex())
112 pnt_list.append(pnt1)
113 w_exp.Next()
114 an_exp.Next()
115 return pnt_list
117 @staticmethod
118 def get_center_of_face(face: TopoDS_Face) -> gp_Pnt:
119 """
120 Calculates the center of the given face. The center point is the center
121 of mass.
122 """
123 prop = GProp_GProps()
124 brepgprop_SurfaceProperties(face, prop)
125 return prop.CentreOfMass()
127 @staticmethod
128 def get_center_of_shape(shape: TopoDS_Shape) -> gp_Pnt:
129 """
130 Calculates the center of the given shape. The center point is the
131 center of mass.
132 """
133 prop = GProp_GProps()
134 brepgprop_VolumeProperties(shape, prop)
135 return prop.CentreOfMass()
137 @staticmethod
138 def get_center_of_edge(edge):
139 """
140 Calculates the center of the given edge. The center point is the center
141 of mass.
142 """
143 prop = GProp_GProps()
144 brepgprop_LinearProperties(edge, prop)
145 return prop.CentreOfMass()
147 @staticmethod
148 def get_center_of_volume(volume: TopoDS_Shape) -> gp_Pnt:
149 """Compute the center of mass of a TopoDS_Shape volume.
151 Args:
152 volume: TopoDS_Shape
154 Returns: gp_Pnt of the center of mass
155 """
156 prop = GProp_GProps()
157 brepgprop_VolumeProperties(volume, prop)
158 return prop.CentreOfMass()
160 @staticmethod
161 def scale_face(face: TopoDS_Face, factor: float,
162 predefined_center: gp_Pnt = None) -> TopoDS_Shape:
163 """
164 Scales the given face by the given factor, using the center of mass of
165 the face as origin of the transformation. If another center than the
166 center of mass should be used for the origin of the transformation,
167 set the predefined_center.
168 """
169 if not predefined_center:
170 center = PyOCCTools.get_center_of_face(face)
171 else:
172 center = predefined_center
173 trsf = gp_Trsf()
174 trsf.SetScale(center, factor)
175 return BRepBuilderAPI_Transform(face, trsf).Shape()
177 @staticmethod
178 def scale_shape(shape: TopoDS_Shape, factor: float,
179 predefined_center: gp_Pnt = None) -> TopoDS_Shape:
180 """
181 Scales the given shape by the given factor, using the center of mass of
182 the shape as origin of the transformation. If another center than the
183 center of mass should be used for the origin of the transformation,
184 set the predefined_center.
185 """
186 if not predefined_center:
187 center = PyOCCTools.get_center_of_volume(shape)
188 else:
189 center = predefined_center
190 trsf = gp_Trsf()
191 trsf.SetScale(center, factor)
192 return BRepBuilderAPI_Transform(shape, trsf).Shape()
194 @staticmethod
195 def scale_edge(edge: TopoDS_Edge, factor: float) -> TopoDS_Shape:
196 """
197 Scales the given edge by the given factor, using the center of mass of
198 the edge as origin of the transformation.
199 """
200 center = PyOCCTools.get_center_of_edge(edge)
201 trsf = gp_Trsf()
202 trsf.SetScale(center, factor)
203 return BRepBuilderAPI_Transform(edge, trsf).Shape()
205 @staticmethod
206 def fix_face(face: TopoDS_Face, tolerance=1e-3) -> TopoDS_Face:
207 """Apply shape healing on a face."""
208 fix = ShapeFix_Face(face)
209 fix.SetMaxTolerance(tolerance)
210 fix.Perform()
211 return fix.Face()
213 @staticmethod
214 def fix_shape(shape: TopoDS_Shape, tolerance=1e-3) -> TopoDS_Shape:
215 """Apply shape healing on a shape."""
216 fix = ShapeFix_Shape(shape)
217 fix.SetFixFreeShellMode(True)
218 fix.LimitTolerance(tolerance)
219 fix.Perform()
220 return fix.Shape()
222 @staticmethod
223 def move_bound_in_direction_of_normal(bound, move_dist: float,
224 reverse=False) -> TopoDS_Shape:
225 """Move a BIM2SIM Space Boundary in the direction of its surface
226 normal by a given distance."""
227 if isinstance(bound, TopoDS_Shape):
228 bound_normal = PyOCCTools.simple_face_normal(bound)
229 bound_shape = bound
230 else:
231 bound_normal = bound.bound_normal
232 bound_shape = bound.bound_shape
233 prod_vec = []
234 move_dir = bound_normal.Coord()
235 if reverse:
236 move_dir = bound_normal.Reversed().Coord()
237 for i in move_dir:
238 prod_vec.append(move_dist * i)
239 # move bound in direction of bound normal by move_dist
240 trsf = gp_Trsf()
241 coord = gp_XYZ(*prod_vec)
242 vec = gp_Vec(coord)
243 trsf.SetTranslation(vec)
244 new_shape = BRepBuilderAPI_Transform(bound_shape, trsf).Shape()
245 return new_shape
247 @staticmethod
248 def compare_direction_of_normals(normal1: gp_XYZ, normal2: gp_XYZ) -> bool:
249 """
250 Compare the direction of two surface normals (vectors).
251 True, if direction is same or reversed
252 :param normal1: first normal (gp_Pnt)
253 :param normal2: second normal (gp_Pnt)
254 :return: True/False
255 """
256 dotp = normal1.Dot(normal2)
257 check = False
258 if 1 - 1e-2 < dotp ** 2 < 1 + 1e-2:
259 check = True
260 return check
262 @staticmethod
263 def _a2p(o, z, x):
264 """Compute Axis of Local Placement of an IfcProducts Objectplacement"""
265 y = np.cross(z, x)
266 r = np.eye(4)
267 r[:-1, :-1] = x, y, z
268 r[-1, :-1] = o
269 return r.T
271 @staticmethod
272 def _axis2placement(plc):
273 """Get Axis of Local Placement of an IfcProducts Objectplacement"""
274 z = np.array(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
275 x = np.array(
276 plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
277 o = plc.Location.Coordinates
278 return PyOCCTools._a2p(o, z, x)
280 @staticmethod
281 def local_placement(plc):
282 """Get Local Placement of an IfcProducts Objectplacement"""
283 if plc.PlacementRelTo is None:
284 parent = np.eye(4)
285 else:
286 parent = PyOCCTools.local_placement(plc.PlacementRelTo)
287 return np.dot(PyOCCTools._axis2placement(plc.RelativePlacement), parent)
289 @staticmethod
290 def simple_face_normal(face: TopoDS_Face, check_orientation: bool = True) \
291 -> gp_XYZ:
292 """Compute the normal of a TopoDS_Face."""
293 face = PyOCCTools.get_face_from_shape(face)
294 surf = BRep_Tool.Surface(face)
295 obj = surf
296 assert obj.DynamicType().Name() == "Geom_Plane"
297 plane = Handle_Geom_Plane_DownCast(surf)
298 face_normal = plane.Axis().Direction().XYZ()
299 if check_orientation:
300 if face.Orientation() == 1:
301 face_normal = face_normal.Reversed()
302 return face_normal
304 @staticmethod
305 def flip_orientation_of_face(face: TopoDS_Face) -> TopoDS_Face:
306 """Flip the orientation of a TopoDS_Face."""
307 face = face.Reversed()
308 return face
310 @staticmethod
311 def get_face_from_shape(shape: TopoDS_Shape) -> TopoDS_Face:
312 """Return first face of a TopoDS_Shape."""
313 exp = TopExp_Explorer(shape, TopAbs_FACE)
314 face = exp.Current()
315 try:
316 face = topods_Face(face)
317 except:
318 exp1 = TopExp_Explorer(shape, TopAbs_WIRE)
319 wire = exp1.Current()
320 face = BRepBuilderAPI_MakeFace(wire).Face()
321 return face
323 @staticmethod
324 def get_faces_from_shape(shape: TopoDS_Shape) -> List[TopoDS_Face]:
325 """Return all faces from a shape."""
326 faces = []
327 an_exp = TopExp_Explorer(shape, TopAbs_FACE)
328 while an_exp.More():
329 face = topods_Face(an_exp.Current())
330 faces.append(face)
331 an_exp.Next()
332 return faces
334 @staticmethod
335 def get_shape_area(shape: TopoDS_Shape) -> float:
336 """compute area of a space boundary"""
337 bound_prop = GProp_GProps()
338 brepgprop_SurfaceProperties(shape, bound_prop)
339 area = bound_prop.Mass()
340 return area
342 @staticmethod
343 def remove_coincident_and_collinear_points_from_face(
344 face: TopoDS_Face) -> TopoDS_Face:
345 """
346 removes collinear and coincident vertices iff resulting number of
347 vertices is > 3, so a valid face can be build.
348 """
349 org_area = PyOCCTools.get_shape_area(face)
350 pnt_list = PyOCCTools.get_points_of_face(face)
351 pnt_list_new = PyOCCTools.remove_coincident_vertices(pnt_list)
352 pnt_list_new = PyOCCTools.remove_collinear_vertices2(pnt_list_new)
353 if pnt_list_new != pnt_list:
354 if len(pnt_list_new) < 3:
355 pnt_list_new = pnt_list
356 new_face = PyOCCTools.make_faces_from_pnts(pnt_list_new)
357 new_area = (PyOCCTools.get_shape_area(new_face))
358 if abs(new_area - org_area) < 5e-3:
359 face = new_face
360 return face
362 @staticmethod
363 def get_shape_volume(shape: TopoDS_Shape) -> float:
364 """
365 This function computes the volume of a shape and returns the value as a
366 float.
367 Args:
368 shape: TopoDS_Shape
369 Returns:
370 volume: float
371 """
372 props = GProp_GProps()
373 brepgprop_VolumeProperties(shape, props)
374 volume = props.Mass()
375 return volume
377 @staticmethod
378 def sew_shapes(shape_list: list[TopoDS_Shape]) -> TopoDS_Shape:
379 sew = BRepBuilderAPI_Sewing(0.0001)
380 for shp in shape_list:
381 sew.Add(shp)
382 sew.Perform()
383 return sew.SewedShape()
385 @staticmethod
386 def move_bounds_to_vertical_pos(bound_list: list(),
387 base_face: TopoDS_Face) -> list[TopoDS_Shape]:
388 new_shape_list = []
389 for bound in bound_list:
390 if not isinstance(bound, TopoDS_Shape):
391 bound_shape = bound.bound_shape
392 else:
393 bound_shape = bound
394 distance = BRepExtrema_DistShapeShape(base_face,
395 bound_shape,
396 Extrema_ExtFlag_MIN).Value()
397 if abs(distance) > 1e-4:
398 new_shape = PyOCCTools.move_bound_in_direction_of_normal(
399 bound, distance)
400 if abs(BRepExtrema_DistShapeShape(
401 base_face, new_shape, Extrema_ExtFlag_MIN).Value()) \
402 > 1e-4:
403 new_shape = PyOCCTools.move_bound_in_direction_of_normal(
404 bound, distance, reverse=True)
405 else:
406 new_shape = bound_shape
407 new_shape_list.append(new_shape)
408 return new_shape_list
410 @staticmethod
411 def get_footprint_of_shape(shape: TopoDS_Shape) -> TopoDS_Face:
412 """
413 Calculate the footprint of a TopoDS_Shape.
414 """
415 footprint_shapes = []
416 return_shape = None
417 faces = PyOCCTools.get_faces_from_shape(shape)
418 for face in faces:
419 prop = BRepGProp_Face(face)
420 p = gp_Pnt()
421 normal_direction = gp_Vec()
422 prop.Normal(0., 0., p, normal_direction)
423 if abs(1 - normal_direction.Z()) < 1e-4:
424 footprint_shapes.append(face)
425 if len(footprint_shapes) == 0:
426 for face in faces:
427 prop = BRepGProp_Face(face)
428 p = gp_Pnt()
429 normal_direction = gp_Vec()
430 prop.Normal(0., 0., p, normal_direction)
431 if abs(1 - abs(normal_direction.Z())) < 1e-4:
432 footprint_shapes.append(face)
433 if len(footprint_shapes) == 1:
434 return_shape = footprint_shapes[0]
435 elif len(footprint_shapes) > 1:
436 bbox = Bnd_Box()
437 brepbndlib_Add(shape, bbox)
438 xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
439 bbox_ground_face = PyOCCTools.make_faces_from_pnts(
440 [(xmin, ymin, zmin),
441 (xmin, ymax, zmin),
442 (xmax, ymax, zmin),
443 (xmax, ymin, zmin)]
444 )
445 footprint_shapes = PyOCCTools.move_bounds_to_vertical_pos(
446 footprint_shapes, bbox_ground_face)
448 return_shape = PyOCCTools.sew_shapes(footprint_shapes)
449 return return_shape
451 @staticmethod
452 def triangulate_bound_shape(shape: TopoDS_Shape,
453 cut_shapes: list[TopoDS_Shape] = [])\
454 -> TopoDS_Shape:
455 """Triangulate bound shape.
457 Args:
458 shape: TopoDS_Shape
459 cut_shapes: list of TopoDS_Shape
460 Returns:
461 Triangulated TopoDS_Shape
463 """
464 if cut_shapes:
465 for cut_shape in cut_shapes:
466 shape = BRepAlgoAPI_Cut(
467 shape, cut_shape).Shape()
468 triang_face = BRepMesh_IncrementalMesh(shape, 1)
469 return triang_face.Shape()
471 @staticmethod
472 def check_pnt_in_solid(solid: TopoDS_Solid, pnt: gp_Pnt, tol=1.0e-6) \
473 -> bool:
474 """Check if a gp_Pnt is inside a TopoDS_Solid.
476 This method checks if a gp_Pnt is included in a TopoDS_Solid. Returns
477 True if gp_Pnt is included, else False.
479 Args:
480 solid: TopoDS_Solid where the gp_Pnt should be included
481 pnt: gp_Pnt that is tested
482 tol: tolerance, default is set to 1e-6
484 Returns: True if gp_Pnt is included in TopoDS_Solid, else False
485 """
486 pnt_in_solid = False
487 classifier = BRepClass3d_SolidClassifier()
488 classifier.Load(solid)
489 classifier.Perform(pnt, tol)
491 if not classifier.State() == TopAbs_OUT: # check if center is in solid
492 pnt_in_solid = True
493 return pnt_in_solid
495 @staticmethod
496 def make_shell_from_faces(faces: list[TopoDS_Face]) -> TopoDS_Shell:
497 """Creates a TopoDS_Shell from a list of TopoDS_Face.
499 Args:
500 faces: list of TopoDS_Face
502 Returns: TopoDS_Shell
503 """
504 shell = BRepBuilderAPI_MakeShell()
505 shell = shell.Shell()
506 builder = TopoDS_Builder()
507 builder.MakeShell(shell)
509 for face in faces:
510 builder.Add(shell, face)
511 return shell
513 @staticmethod
514 def make_solid_from_shell(shell: TopoDS_Shell) -> TopoDS_Solid:
515 """Create a TopoDS_Solid from a given TopoDS_Shell.
517 Args:
518 shell: TopoDS_Shell
520 Returns: TopoDS_Solid
521 """
522 solid = BRepBuilderAPI_MakeSolid()
523 solid.Add(shell)
524 return solid.Solid()
526 def make_solid_from_shape(self, base_shape: TopoDS_Shape) -> TopoDS_Solid:
527 """Make a TopoDS_Solid from a TopoDS_Shape.
529 Args:
530 base_shape: TopoDS_Shape
532 Returns: TopoDS_Solid
534 """
535 faces = self.get_faces_from_shape(base_shape)
536 shell = self.make_shell_from_faces(faces)
537 return self.make_solid_from_shell(shell)
539 @staticmethod
540 def obj2_in_obj1(obj1: TopoDS_Shape, obj2: TopoDS_Shape) -> bool:
541 """ Checks if the center of obj2 is actually in the shape of obj1.
543 This method is used to compute if the center of mass of a TopoDS_Shape
544 is included in another TopoDS_Shape. This can be used to determine,
545 if a HVAC element (e.g., IfcSpaceHeater) is included in the
546 TopoDS_Shape of an IfcSpace.
548 Args:
549 obj1: TopoDS_Shape of the larger element (e.g., IfcSpace)
550 obj2: TopoDS_Shape of the smaller element (e.g., IfcSpaceHeater,
551 IfcAirTerminal)
553 Returns: True if obj2 is in obj1, else False
554 """
555 faces = PyOCCTools.get_faces_from_shape(obj1)
556 shell = PyOCCTools.make_shell_from_faces(faces)
557 obj1_solid = PyOCCTools.make_solid_from_shell(shell)
558 obj2_center = PyOCCTools.get_center_of_volume(obj2)
560 return PyOCCTools.check_pnt_in_solid(obj1_solid, obj2_center)