Coverage for bim2sim / utilities / pyocc_tools.py: 33%
651 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-08 16:42 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-08 16:42 +0000
1"""
2Common tools for handling OCC Shapes within the bim2sim project.
3"""
4import math
5from typing import List, Tuple, Union
7import numpy as np
8from OCC.Core.BRep import BRep_Tool
9from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
10from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse
11from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeOffsetShape
12from OCC.Core.GeomAPI import GeomAPI_IntCS
13from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
14from scipy.spatial import KDTree
15from OCC.Core.BRepBndLib import brepbndlib
16from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, \
17 BRepBuilderAPI_Transform, BRepBuilderAPI_MakePolygon, \
18 BRepBuilderAPI_MakeShell, BRepBuilderAPI_MakeSolid, BRepBuilderAPI_Sewing, \
19 BRepBuilderAPI_MakeVertex
20from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier
21from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
22from OCC.Core.BRepGProp import brepgprop, BRepGProp_Face
23from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
24from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakePrism
25from OCC.Core.BRepTools import BRepTools_WireExplorer
26from OCC.Core.Bnd import Bnd_Box
27from OCC.Core.Extrema import Extrema_ExtFlag_MIN
28from OCC.Core.GProp import GProp_GProps
29from OCC.Core.Geom import Geom_Plane, Geom_Line, \
30 Geom_Curve, Geom_Surface
31from OCC.Core.ShapeAnalysis import ShapeAnalysis_ShapeContents
32from OCC.Core.ShapeFix import ShapeFix_Face, ShapeFix_Shape
33from OCC.Core.TopAbs import TopAbs_WIRE, TopAbs_FACE, TopAbs_OUT
34from OCC.Core.TopExp import TopExp_Explorer
35from OCC.Core.TopoDS import topods, TopoDS_Shape, \
36 TopoDS_Face, TopoDS_Edge, TopoDS_Solid, TopoDS_Shell, TopoDS_Builder, \
37 TopoDS_Compound
38from OCC.Core.gp import gp_XYZ, gp_Pnt, gp_Trsf, gp_Vec, gp_Ax1, gp_Dir, gp_Lin
41class PyOCCTools:
42 """Class for Tools handling and modifying Python OCC Shapes"""
44 @staticmethod
45 def remove_coincident_vertices(vert_list: List[gp_Pnt]) -> List[gp_Pnt]:
46 """ remove coincident vertices from list of gp_Pnt.
47 Vertices are coincident if closer than tolerance."""
48 tol_dist = 1e-2
49 new_list = []
50 v_b = np.array(vert_list[-1].Coord())
51 for vert in vert_list:
52 v = np.array(vert.Coord())
53 d_b = np.linalg.norm(v - v_b)
54 if d_b > tol_dist:
55 new_list.append(vert)
56 v_b = v
57 return new_list
59 @staticmethod
60 def remove_collinear_vertices2(vert_list: List[gp_Pnt]) -> List[gp_Pnt]:
61 """ remove collinear vertices from list of gp_Pnt.
62 Vertices are collinear if cross product less tolerance."""
63 tol_cross = 1e-3
64 new_list = []
66 for i, vert in enumerate(vert_list):
67 v = np.array(vert.Coord())
68 v_b = np.array(vert_list[(i - 1) % (len(vert_list))].Coord())
69 v_f = np.array(vert_list[(i + 1) % (len(vert_list))].Coord())
70 v1 = v - v_b
71 v2 = v_f - v_b
72 if np.linalg.norm(np.cross(v1, v2)) / np.linalg.norm(
73 v2) > tol_cross:
74 new_list.append(vert)
75 return new_list
77 @staticmethod
78 def make_faces_from_pnts(
79 pnt_list: Union[List[Tuple[float]], List[gp_Pnt]]) -> TopoDS_Face:
80 """
81 This function returns a TopoDS_Face from list of gp_Pnt
82 :param pnt_list: list of gp_Pnt or Coordinate-Tuples
83 :return: TopoDS_Face
84 """
85 if isinstance(pnt_list[0], tuple):
86 new_list = []
87 for pnt in pnt_list:
88 new_list.append(gp_Pnt(gp_XYZ(pnt[0], pnt[1], pnt[2])))
89 pnt_list = new_list
90 poly = BRepBuilderAPI_MakePolygon()
91 for coord in pnt_list:
92 poly.Add(coord)
93 poly.Close()
94 a_wire = poly.Wire()
95 a_face = BRepBuilderAPI_MakeFace(a_wire).Face()
96 return a_face
98 @staticmethod
99 def get_number_of_vertices(shape: TopoDS_Shape) -> int:
100 """ get number of vertices of a shape"""
101 shape_analysis = ShapeAnalysis_ShapeContents()
102 shape_analysis.Perform(shape)
103 nb_vertex = shape_analysis.NbVertices()
105 return nb_vertex
107 @staticmethod
108 def get_number_of_faces(shape: TopoDS_Shape) -> int:
109 """ get number of faces of a shape"""
110 shape_analysis = ShapeAnalysis_ShapeContents()
111 shape_analysis.Perform(shape)
112 nb_faces = shape_analysis.NbFaces()
114 return nb_faces
116 @staticmethod
117 def get_points_of_face(shape: TopoDS_Shape) -> List[gp_Pnt]:
118 """
119 This function returns a list of gp_Pnt of a Surface
120 :param shape: TopoDS_Shape (Surface)
121 :return: pnt_list (list of gp_Pnt)
122 """
123 an_exp = TopExp_Explorer(shape, TopAbs_WIRE)
124 pnt_list = []
125 while an_exp.More():
126 wire = topods.Wire(an_exp.Current())
127 w_exp = BRepTools_WireExplorer(wire)
128 while w_exp.More():
129 pnt1 = BRep_Tool.Pnt(w_exp.CurrentVertex())
130 pnt_list.append(pnt1)
131 w_exp.Next()
132 an_exp.Next()
133 return pnt_list
135 @staticmethod
136 def get_center_of_face(face: TopoDS_Face) -> gp_Pnt:
137 """
138 Calculates the center of the given face. The center point is the center
139 of mass.
140 """
141 prop = GProp_GProps()
142 brepgprop.SurfaceProperties(face, prop)
143 return prop.CentreOfMass()
145 @staticmethod
146 def get_center_of_shape(shape: TopoDS_Shape) -> gp_Pnt:
147 """
148 Calculates the center of the given shape. The center point is the
149 center of mass.
150 """
151 prop = GProp_GProps()
152 brepgprop.VolumeProperties(shape, prop)
153 return prop.CentreOfMass()
155 @staticmethod
156 def get_center_of_edge(edge):
157 """
158 Calculates the center of the given edge. The center point is the center
159 of mass.
160 """
161 prop = GProp_GProps()
162 brepgprop.LinearProperties(edge, prop)
163 return prop.CentreOfMass()
165 @staticmethod
166 def get_center_of_volume(volume: TopoDS_Shape) -> gp_Pnt:
167 """Compute the center of mass of a TopoDS_Shape volume.
169 Args:
170 volume: TopoDS_Shape
172 Returns: gp_Pnt of the center of mass
173 """
174 prop = GProp_GProps()
175 brepgprop.VolumeProperties(volume, prop)
176 return prop.CentreOfMass()
178 @staticmethod
179 def scale_face(face: TopoDS_Face, factor: float,
180 predefined_center: gp_Pnt = None) -> TopoDS_Shape:
181 """
182 Scales the given face by the given factor, using the center of mass of
183 the face as origin of the transformation. If another center than the
184 center of mass should be used for the origin of the transformation,
185 set the predefined_center.
186 """
187 if not predefined_center:
188 center = PyOCCTools.get_center_of_face(face)
189 else:
190 center = predefined_center
191 trsf = gp_Trsf()
192 trsf.SetScale(center, factor)
193 return BRepBuilderAPI_Transform(face, trsf).Shape()
195 @staticmethod
196 def scale_shape(shape: TopoDS_Shape, factor: float,
197 predefined_center: gp_Pnt = None) -> TopoDS_Shape:
198 """
199 Scales the given shape by the given factor, using the center of mass of
200 the shape as origin of the transformation. If another center than the
201 center of mass should be used for the origin of the transformation,
202 set the predefined_center.
203 """
204 if not predefined_center:
205 center = PyOCCTools.get_center_of_volume(shape)
206 else:
207 center = predefined_center
208 trsf = gp_Trsf()
209 trsf.SetScale(center, factor)
210 return BRepBuilderAPI_Transform(shape, trsf).Shape()
212 @staticmethod
213 def scale_shape_absolute(shape: TopoDS_Shape, scale_in_meters: float,
214 predefined_center: gp_Pnt = None):
215 """
216 Scales the given shape by the given distance in all directions.
217 Using the center of mass of the shape as origin of the
218 transformation. If another center than the center of mass should be
219 used for the origin of the transformation,
220 set the predefined_center.
221 Args:
222 shape:
223 scale_in_meters: scale in meters, scaling is applied in each
224 direction.
225 predefined_center:
227 Returns:
229 """
230 (min_x, min_y, min_z), (max_x, max_y, max_z) = (
231 PyOCCTools.simple_bounding_box(shape))
232 original_size = min(max_x - min_x, max_y - min_y, max_z - min_z)
233 new_size = original_size + scale_in_meters * 2
234 scaling_factor = new_size / original_size
235 return PyOCCTools.scale_shape(shape, scaling_factor)
237 @staticmethod
238 def scale_edge(edge: TopoDS_Edge, factor: float) -> TopoDS_Shape:
239 """
240 Scales the given edge by the given factor, using the center of mass of
241 the edge as origin of the transformation.
242 """
243 center = PyOCCTools.get_center_of_edge(edge)
244 trsf = gp_Trsf()
245 trsf.SetScale(center, factor)
246 return BRepBuilderAPI_Transform(edge, trsf).Shape()
248 @staticmethod
249 def fix_face(face: TopoDS_Face, tolerance=1e-3) -> TopoDS_Face:
250 """Apply shape healing on a face."""
251 fix = ShapeFix_Face(face)
252 fix.SetMaxTolerance(tolerance)
253 fix.Perform()
254 return fix.Face()
256 @staticmethod
257 def fix_shape(shape: TopoDS_Shape, tolerance=1e-3) -> TopoDS_Shape:
258 """Apply shape healing on a shape."""
259 fix = ShapeFix_Shape(shape)
260 fix.SetFixFreeShellMode(True)
261 fix.LimitTolerance(tolerance)
262 fix.Perform()
263 return fix.Shape()
265 @staticmethod
266 def move_bound_in_direction_of_normal(bound, move_dist: float,
267 reverse=False, move_dir:
268 gp_Dir=None) -> (TopoDS_Shape):
269 """Move a BIM2SIM Space Boundary in the direction of its surface
270 normal by a given distance."""
271 if not move_dir:
272 if isinstance(bound, TopoDS_Shape):
273 bound_normal = PyOCCTools.simple_face_normal(bound)
274 bound_shape = bound
275 else:
276 bound_normal = bound.bound_normal
277 bound_shape = bound.bound_shape
278 move_dir = bound_normal.Coord()
279 else:
280 move_dir = move_dir.Coord()
281 bound_shape=bound
282 prod_vec = []
283 if reverse:
284 move_dir = gp_Vec(*move_dir).Reversed().Coord()
285 for i in move_dir:
286 prod_vec.append(move_dist * i)
287 # move bound in direction of bound normal by move_dist
288 trsf = gp_Trsf()
289 coord = gp_XYZ(*prod_vec)
290 vec = gp_Vec(coord)
291 trsf.SetTranslation(vec)
292 new_shape = BRepBuilderAPI_Transform(bound_shape, trsf).Shape()
293 return new_shape
295 @staticmethod
296 def compare_direction_of_normals(normal1: gp_XYZ, normal2: gp_XYZ) -> bool:
297 """
298 Compare the direction of two surface normals (vectors).
299 True, if direction is same or reversed
300 :param normal1: first normal (gp_Pnt)
301 :param normal2: second normal (gp_Pnt)
302 :return: True/False
303 """
304 dotp = normal1.Dot(normal2)
305 check = False
306 if 1 - 1e-2 < dotp ** 2 < 1 + 1e-2:
307 check = True
308 return check
310 @staticmethod
311 def _a2p(o, z, x):
312 """Compute Axis of Local Placement of an IfcProducts Objectplacement"""
313 y = np.cross(z, x)
314 r = np.eye(4)
315 r[:-1, :-1] = x, y, z
316 r[-1, :-1] = o
317 return r.T
319 @staticmethod
320 def _axis2placement(plc):
321 """Get Axis of Local Placement of an IfcProducts Objectplacement"""
322 z = np.array(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
323 x = np.array(
324 plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
325 o = plc.Location.Coordinates
326 return PyOCCTools._a2p(o, z, x)
328 @staticmethod
329 def local_placement(plc):
330 """Get Local Placement of an IfcProducts Objectplacement"""
331 if plc.PlacementRelTo is None:
332 parent = np.eye(4)
333 else:
334 parent = PyOCCTools.local_placement(plc.PlacementRelTo)
335 return np.dot(PyOCCTools._axis2placement(plc.RelativePlacement), parent)
337 @staticmethod
338 def simple_face_normal(face: TopoDS_Face, check_orientation: bool = True) \
339 -> gp_XYZ:
340 """Compute the normal of a TopoDS_Face."""
341 face = PyOCCTools.get_face_from_shape(face)
342 surf = BRep_Tool.Surface(face)
343 obj = surf
344 assert obj.DynamicType().Name() == "Geom_Plane"
345 plane = Geom_Plane.DownCast(surf)
346 face_normal = plane.Axis().Direction().XYZ()
347 if check_orientation:
348 if face.Orientation() == 1:
349 face_normal = face_normal.Reversed()
350 return face_normal
352 @staticmethod
353 def flip_orientation_of_face(face: TopoDS_Face) -> TopoDS_Face:
354 """Flip the orientation of a TopoDS_Face."""
355 face = face.Reversed()
356 return face
358 @staticmethod
359 def get_face_from_shape(shape: TopoDS_Shape) -> TopoDS_Face:
360 """Return first face of a TopoDS_Shape."""
361 exp = TopExp_Explorer(shape, TopAbs_FACE)
362 face = topods.Face(exp.Current())
363 try:
364 face = topods.Face(face)
365 except:
366 exp1 = TopExp_Explorer(shape, TopAbs_WIRE)
367 wire = topods.Wire(exp1.Current())
368 face = BRepBuilderAPI_MakeFace(wire).Face()
369 return face
371 @staticmethod
372 def get_faces_from_shape(shape: TopoDS_Shape) -> List[TopoDS_Face]:
373 """Return all faces from a shape."""
374 faces = []
375 an_exp = TopExp_Explorer(shape, TopAbs_FACE)
376 while an_exp.More():
377 face = topods.Face(an_exp.Current())
378 faces.append(face)
379 an_exp.Next()
380 return faces
382 @staticmethod
383 def get_shape_area(shape: TopoDS_Shape) -> float:
384 """compute area of a space boundary"""
385 bound_prop = GProp_GProps()
386 brepgprop.SurfaceProperties(shape, bound_prop)
387 area = bound_prop.Mass()
388 return area
390 @staticmethod
391 def remove_coincident_and_collinear_points_from_face(
392 face: TopoDS_Face) -> TopoDS_Face:
393 """
394 removes collinear and coincident vertices iff resulting number of
395 vertices is > 3, so a valid face can be build.
396 """
397 org_area = PyOCCTools.get_shape_area(face)
398 pnt_list = PyOCCTools.get_points_of_face(face)
399 pnt_list_new = PyOCCTools.remove_coincident_vertices(pnt_list)
400 pnt_list_new = PyOCCTools.remove_collinear_vertices2(pnt_list_new)
401 if pnt_list_new != pnt_list:
402 if len(pnt_list_new) < 3:
403 pnt_list_new = pnt_list
404 new_face = PyOCCTools.make_faces_from_pnts(pnt_list_new)
405 new_area = (PyOCCTools.get_shape_area(new_face))
406 if abs(new_area - org_area) < 5e-3:
407 face = new_face
408 return face
410 @staticmethod
411 def get_shape_volume(shape: TopoDS_Shape) -> float:
412 """
413 This function computes the volume of a shape and returns the value as a
414 float.
416 Args:
417 shape: TopoDS_Shape
419 Returns:
420 volume: float
421 """
422 props = GProp_GProps()
423 brepgprop.VolumeProperties(shape, props)
424 volume = props.Mass()
425 return volume
427 @staticmethod
428 def sew_shapes(shape_list: list[TopoDS_Shape], tolerance=0.0001) -> (
429 TopoDS_Shape):
430 sew = BRepBuilderAPI_Sewing(tolerance)
431 for shp in shape_list:
432 sew.Add(shp)
433 sew.Perform()
434 return sew.SewedShape()
436 @staticmethod
437 def get_points_of_minimum_shape_distance(
438 shape1: TopoDS_Shape, shape2: TopoDS_Shape) -> list[list[gp_Pnt,
439 gp_Pnt, float]]:
440 """
441 Compute points of minimum distance.
443 Returns list of [point on first shape, point on second shape,
444 distance between these points].
445 """
446 minimum_point_pairs = []
447 extrema = BRepExtrema_DistShapeShape(shape1, shape2,
448 Extrema_ExtFlag_MIN)
449 # Perform the computation
450 extrema.Perform()
451 # Check if the computation was successful
452 if extrema.IsDone():
453 # Get the number of solution pairs (usually 1 for minimum distance)
454 nb_extrema = extrema.NbSolution()
455 # print(f"Number of minimum distance solutions: {nb_extrema}")
456 for i in range(1,
457 nb_extrema + 1): # OpenCASCADE is 1-based indexing
458 # Retrieve the points on each shape
459 p1 = extrema.PointOnShape1(i)
460 p2 = extrema.PointOnShape2(i)
461 minimum_point_pairs.append([p1, p2, extrema.Value()])
462 return minimum_point_pairs
464 @staticmethod
465 def get_points_of_minimum_point_shape_distance(
466 point: gp_Pnt, shape: TopoDS_Shape) -> list[list[gp_Pnt, gp_Pnt,
467 float]]:
469 vertex = BRepBuilderAPI_MakeVertex(point).Vertex()
470 minimum_point_pairs = PyOCCTools.get_points_of_minimum_shape_distance(
471 vertex, shape)
472 return minimum_point_pairs
474 @staticmethod
475 def move_bounds_to_vertical_pos(bound_list: list(),
476 base_face: TopoDS_Face) -> list[
477 TopoDS_Shape]:
478 new_shape_list = []
479 for bound in bound_list:
480 if not isinstance(bound, TopoDS_Shape):
481 bound_shape = bound.bound_shape
482 else:
483 bound_shape = bound
484 distance = BRepExtrema_DistShapeShape(base_face,
485 bound_shape,
486 Extrema_ExtFlag_MIN).Value()
487 if abs(distance) > 1e-4:
488 new_shape = PyOCCTools.move_bound_in_direction_of_normal(
489 bound, distance)
490 if abs(BRepExtrema_DistShapeShape(
491 base_face, new_shape, Extrema_ExtFlag_MIN).Value()) \
492 > 1e-4:
493 new_shape = PyOCCTools.move_bound_in_direction_of_normal(
494 bound, distance, reverse=True)
495 else:
496 new_shape = bound_shape
497 new_shape_list.append(new_shape)
498 return new_shape_list
500 @staticmethod
501 def get_footprint_of_shape(shape: TopoDS_Shape) -> TopoDS_Face:
502 """
503 Calculate the footprint of a TopoDS_Shape.
504 """
505 footprint_shapes = []
506 return_shape = None
507 faces = PyOCCTools.get_faces_from_shape(shape)
508 for face in faces:
509 prop = BRepGProp_Face(face)
510 p = gp_Pnt()
511 normal_direction = gp_Vec()
512 prop.Normal(0., 0., p, normal_direction)
513 if abs(1 - normal_direction.Z()) < 1e-4:
514 footprint_shapes.append(face)
515 if len(footprint_shapes) == 0:
516 for face in faces:
517 prop = BRepGProp_Face(face)
518 p = gp_Pnt()
519 normal_direction = gp_Vec()
520 prop.Normal(0., 0., p, normal_direction)
521 if abs(1 - abs(normal_direction.Z())) < 1e-4:
522 footprint_shapes.append(face)
523 if len(footprint_shapes) == 1:
524 return_shape = footprint_shapes[0]
525 elif len(footprint_shapes) > 1:
526 bbox = Bnd_Box()
527 brepbndlib.Add(shape, bbox)
528 xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
529 bbox_ground_face = PyOCCTools.make_faces_from_pnts(
530 [(xmin, ymin, zmin),
531 (xmin, ymax, zmin),
532 (xmax, ymax, zmin),
533 (xmax, ymin, zmin)]
534 )
535 footprint_shapes = PyOCCTools.move_bounds_to_vertical_pos(
536 footprint_shapes, bbox_ground_face)
538 return_shape = PyOCCTools.sew_shapes(footprint_shapes)
539 return return_shape
541 @staticmethod
542 def triangulate_bound_shape(shape: TopoDS_Shape,
543 cut_shapes: list[TopoDS_Shape] = []) \
544 -> TopoDS_Shape:
545 """Triangulate bound shape.
547 Args:
548 shape: TopoDS_Shape
549 cut_shapes: list of TopoDS_Shape
550 Returns:
551 Triangulated TopoDS_Shape
553 """
554 if cut_shapes:
555 for cut_shape in cut_shapes:
556 shape = BRepAlgoAPI_Cut(
557 shape, cut_shape).Shape()
558 triang_face = BRepMesh_IncrementalMesh(shape, 1)
559 return triang_face.Shape()
561 @staticmethod
562 def check_pnt_in_solid(solid: TopoDS_Solid, pnt: gp_Pnt, tol=1.0e-6) \
563 -> bool:
564 """Check if a gp_Pnt is inside a TopoDS_Solid.
566 This method checks if a gp_Pnt is included in a TopoDS_Solid. Returns
567 True if gp_Pnt is included, else False.
569 Args:
570 solid: TopoDS_Solid where the gp_Pnt should be included
571 pnt: gp_Pnt that is tested
572 tol: tolerance, default is set to 1e-6
574 Returns: True if gp_Pnt is included in TopoDS_Solid, else False
575 """
576 pnt_in_solid = False
577 classifier = BRepClass3d_SolidClassifier()
578 classifier.Load(solid)
579 classifier.Perform(pnt, tol)
581 if not classifier.State() == TopAbs_OUT: # check if center is in solid
582 pnt_in_solid = True
583 return pnt_in_solid
585 @staticmethod
586 def make_shell_from_faces(faces: list[TopoDS_Face]) -> TopoDS_Shell:
587 """Creates a TopoDS_Shell from a list of TopoDS_Face.
589 Args:
590 faces: list of TopoDS_Face
592 Returns: TopoDS_Shell
593 """
594 shell = BRepBuilderAPI_MakeShell()
595 shell = shell.Shell()
596 builder = TopoDS_Builder()
597 builder.MakeShell(shell)
599 for face in faces:
600 builder.Add(shell, face)
601 return shell
603 @staticmethod
604 def make_solid_from_shell(shell: TopoDS_Shell) -> TopoDS_Solid:
605 """Create a TopoDS_Solid from a given TopoDS_Shell.
607 Args:
608 shell: TopoDS_Shell
610 Returns: TopoDS_Solid
611 """
612 solid = BRepBuilderAPI_MakeSolid()
613 solid.Add(shell)
614 return solid.Solid()
616 def make_solid_from_shape(self, base_shape: TopoDS_Shape) -> TopoDS_Solid:
617 """Make a TopoDS_Solid from a TopoDS_Shape.
619 Args:
620 base_shape: TopoDS_Shape
622 Returns: TopoDS_Solid
624 """
625 faces = self.get_faces_from_shape(base_shape)
626 shell = self.make_shell_from_faces(faces)
627 return self.make_solid_from_shell(shell)
629 @staticmethod
630 def obj2_in_obj1(obj1: TopoDS_Shape, obj2: TopoDS_Shape) -> bool:
631 """ Checks if the center of obj2 is actually in the shape of obj1.
633 This method is used to compute if the center of mass of a TopoDS_Shape
634 is included in another TopoDS_Shape. This can be used to determine,
635 if a HVAC element (e.g., IfcSpaceHeater) is included in the
636 TopoDS_Shape of an IfcSpace.
638 Args:
639 obj1: TopoDS_Shape of the larger element (e.g., IfcSpace)
640 obj2: TopoDS_Shape of the smaller element (e.g., IfcSpaceHeater,
641 IfcAirTerminal)
643 Returns: True if obj2 is in obj1, else False
644 """
645 faces = PyOCCTools.get_faces_from_shape(obj1)
646 shell = PyOCCTools.make_shell_from_faces(faces)
647 obj1_solid = PyOCCTools.make_solid_from_shell(shell)
648 obj2_center = PyOCCTools.get_center_of_volume(obj2)
650 return PyOCCTools.check_pnt_in_solid(obj1_solid, obj2_center)
652 @staticmethod
653 def get_minimal_bounding_box(shape):
654 # Create an empty bounding box
655 bbox = Bnd_Box()
657 an_exp = TopExp_Explorer(shape, TopAbs_FACE)
658 while an_exp.More():
659 face = topods.Face(an_exp.Current())
660 brepbndlib.Add(face, bbox)
661 an_exp.Next()
663 # Get the minimal bounding box
664 min_x, min_y, min_z, max_x, max_y, max_z = bbox.Get()
666 return (min_x, min_y, min_z), (max_x, max_y, max_z)
668 @staticmethod
669 def simple_bounding_box(shapes: Union[TopoDS_Shape, List[TopoDS_Shape]]) \
670 -> tuple[tuple[float, float, float], tuple[float, float, float]]:
671 """Simple Bounding box.
673 Return min_max_coordinates of a simple Bounding box, either from a
674 single TopoDS_Shape or a list of TopoDS_Shapes
675 """
676 bbox = Bnd_Box()
677 if isinstance(shapes, TopoDS_Shape):
678 brepbndlib.Add(shapes, bbox)
679 else:
680 for shape in shapes:
681 brepbndlib.Add(shape, bbox)
682 min_x, min_y, min_z, max_x, max_y, max_z = bbox.Get()
684 return (min_x, min_y, min_z), (max_x, max_y, max_z)
686 @staticmethod
687 def simple_bounding_box_shape(
688 shapes: Union[TopoDS_Shape, List[TopoDS_Shape]]):
689 min_box, max_box = PyOCCTools.simple_bounding_box(shapes)
690 return BRepPrimAPI_MakeBox(gp_Pnt(*min_box), gp_Pnt(*max_box)).Shape()
692 @staticmethod
693 def get_unique_vertices(edges: list) -> list:
694 """Get unique vertices from a list of edges."""
695 unique_vertices = []
696 for edge in edges:
697 for vertex in edge:
698 if vertex not in unique_vertices:
699 unique_vertices.append(vertex)
700 unique_vertices = [gp_Pnt(v[0], v[1], v[2]) for v in unique_vertices]
701 return unique_vertices
703 @staticmethod
704 def remove_sides_of_bounding_box(shape, cut_top=False, cut_bottom=True,
705 cut_left=False, cut_right=False,
706 cut_back=False, cut_front=False):
707 shape_list = []
708 removed_list = []
709 bbox_shape = PyOCCTools.simple_bounding_box(shape)
710 top_surface_min_max = ((bbox_shape[0][0],
711 bbox_shape[0][1],
712 bbox_shape[1][2]),
713 (bbox_shape[1][0],
714 bbox_shape[1][1],
715 bbox_shape[1][2]))
716 top_surface = PyOCCTools.make_faces_from_pnts([
717 gp_Pnt(*top_surface_min_max[0]),
718 gp_Pnt(top_surface_min_max[1][0],
719 top_surface_min_max[0][1],
720 top_surface_min_max[0][2]),
721 gp_Pnt(*top_surface_min_max[1]),
722 gp_Pnt(top_surface_min_max[0][0],
723 top_surface_min_max[1][1],
724 top_surface_min_max[1][2])])
725 if not cut_top:
726 shape_list.append(top_surface)
727 else:
728 removed_list.append(top_surface)
729 bottom_surface_min_max = ((bbox_shape[0][0],
730 bbox_shape[0][1],
731 bbox_shape[0][2]),
732 (bbox_shape[1][0],
733 bbox_shape[1][1],
734 bbox_shape[0][2]))
735 bottom_surface = PyOCCTools.make_faces_from_pnts([
736 gp_Pnt(*bottom_surface_min_max[0]),
737 gp_Pnt(bottom_surface_min_max[1][0],
738 bottom_surface_min_max[0][1],
739 bottom_surface_min_max[0][2]),
740 gp_Pnt(*bottom_surface_min_max[1]),
741 gp_Pnt(bottom_surface_min_max[0][0],
742 bottom_surface_min_max[1][1],
743 bottom_surface_min_max[1][2])])
744 if not cut_bottom:
745 shape_list.append(bottom_surface)
746 else:
747 removed_list.append(bottom_surface)
748 front_surface_min_max = (
749 bbox_shape[0],
750 (bbox_shape[1][0],
751 bbox_shape[0][1],
752 bbox_shape[1][2]))
753 front_surface = PyOCCTools.make_faces_from_pnts([
754 gp_Pnt(*front_surface_min_max[0]),
755 gp_Pnt(front_surface_min_max[1][0],
756 front_surface_min_max[0][1],
757 front_surface_min_max[0][2]),
758 gp_Pnt(*front_surface_min_max[1]),
759 gp_Pnt(front_surface_min_max[0][0],
760 front_surface_min_max[0][1],
761 front_surface_min_max[1][2])])
762 if not cut_front:
763 shape_list.append(front_surface)
764 else:
765 removed_list.append(front_surface)
766 back_surface_min_max = ((bbox_shape[0][0],
767 bbox_shape[1][1],
768 bbox_shape[0][2]),
769 bbox_shape[1])
770 back_surface = PyOCCTools.make_faces_from_pnts([
771 gp_Pnt(*back_surface_min_max[0]),
772 gp_Pnt(back_surface_min_max[1][0],
773 back_surface_min_max[0][1],
774 back_surface_min_max[0][2]),
775 gp_Pnt(*back_surface_min_max[1]),
776 gp_Pnt(back_surface_min_max[0][0],
777 back_surface_min_max[0][1],
778 back_surface_min_max[1][2])])
780 if not cut_back:
781 shape_list.append(back_surface)
782 else:
783 removed_list.append(back_surface)
784 side_surface_left = PyOCCTools.make_faces_from_pnts([
785 gp_Pnt(*back_surface_min_max[0]),
786 gp_Pnt(*front_surface_min_max[0]),
787 gp_Pnt(front_surface_min_max[0][0],
788 front_surface_min_max[0][1],
789 front_surface_min_max[1][2]),
790 gp_Pnt(back_surface_min_max[0][0],
791 back_surface_min_max[0][1],
792 back_surface_min_max[1][2])]
793 )
794 if not cut_left:
795 shape_list.append(side_surface_left)
796 else:
797 removed_list.append(side_surface_left)
798 side_surface_right = PyOCCTools.make_faces_from_pnts([
799 gp_Pnt(back_surface_min_max[1][0],
800 back_surface_min_max[1][1],
801 back_surface_min_max[0][2]),
802 gp_Pnt(front_surface_min_max[1][0],
803 front_surface_min_max[1][1],
804 front_surface_min_max[0][2]),
805 gp_Pnt(*front_surface_min_max[1]),
806 gp_Pnt(*back_surface_min_max[1])]
807 )
808 if not cut_right:
809 shape_list.append(side_surface_right)
810 else:
811 removed_list.append(side_surface_right)
812 compound = TopoDS_Compound()
813 builder = TopoDS_Builder()
814 builder.MakeCompound(compound)
815 for shp in shape_list:
816 builder.Add(compound, shp)
817 return compound, shape_list, removed_list
819 @staticmethod
820 def rotate_by_deg(shape, axis='z', rotation=90):
821 """
823 Args:
824 shape:
825 axis:
826 rotation:
828 Returns:
830 """
831 rot_center = PyOCCTools.get_center_of_face(shape)
832 rot_ax = None
833 if axis == 'x':
834 rot_ax = gp_Ax1(rot_center, gp_Dir(1, 0, 0))
835 if axis == 'y':
836 rot_ax = gp_Ax1(rot_center, gp_Dir(0, 1, 0))
837 if axis == 'z':
838 rot_ax = gp_Ax1(rot_center, gp_Dir(0, 0, 1))
840 trsf = gp_Trsf()
841 trsf.SetRotation(rot_ax, rotation * math.pi / 180)
842 new_shape = BRepBuilderAPI_Transform(shape, trsf).Shape()
843 return new_shape
845 @staticmethod
846 def generate_obj_trsfs(final_obj_locations: list[gp_Pnt],
847 org_obj_pos: gp_Pnt, rot_angle=0):
848 obj_trsfs = []
849 angle_radians = math.radians(rot_angle)
850 for loc in final_obj_locations:
851 trsf1 = gp_Trsf()
852 trsf2 = gp_Trsf()
853 trsf1.SetTranslation(org_obj_pos, loc)
854 rotation_axis = gp_Ax1(org_obj_pos,
855 gp_Dir(0, 0, 1))
856 trsf2.SetRotation(rotation_axis, angle_radians)
857 trsf = trsf1.Multiplied(trsf2)
858 obj_trsfs.append(trsf)
859 return obj_trsfs
861 @staticmethod
862 def sample_points_on_faces(shape, u_samples=10, v_samples=10):
863 """
864 Generate a grid of points on the surfaces of a shape.
865 Parameters:
866 - shape: TopoDS_Shape
867 - u_samples: Number of samples along the U direction
868 - v_samples: Number of samples along the V direction
869 Returns:
870 A list of (x, y, z) points.
871 """
872 points = []
873 explorer = TopExp_Explorer(shape, TopAbs_FACE)
874 while explorer.More():
875 face = topods.Face(explorer.Current())
876 surface = BRepAdaptor_Surface(face)
877 # Get the parameter range of the surface
878 u_min, u_max = surface.FirstUParameter(), surface.LastUParameter()
879 v_min, v_max = surface.FirstVParameter(), surface.LastVParameter()
880 # Generate a grid of parameters
881 u_values = np.linspace(u_min, u_max, u_samples)
882 v_values = np.linspace(v_min, v_max, v_samples)
883 # Evaluate the surface at each grid point
884 for u in u_values:
885 for v in v_values:
886 pnt = surface.Value(u, v)
887 points.append((pnt.X(), pnt.Y(), pnt.Z()))
888 explorer.Next()
889 return points
891 @staticmethod
892 def calculate_point_based_distance(shape1, shape2, final_num_points=1e5):
893 num_verts_1 = PyOCCTools.get_number_of_vertices(shape1)
894 if num_verts_1 < 5e4:
895 num_faces_1 = PyOCCTools.get_number_of_faces(shape1)
896 sample_points_per_face = math.floor(math.sqrt((
897 final_num_points - num_verts_1)
898 / num_faces_1))
899 points_on_shape1 = PyOCCTools.sample_points_on_faces(
900 shape1, u_samples=sample_points_per_face,
901 v_samples=sample_points_per_face)
902 else:
903 points_on_shape1 = PyOCCTools.get_points_of_face(shape1)
904 points_on_shape1 = [(p.X(), p.Y(), p.Z()) for p in points_on_shape1]
906 num_verts_2 = PyOCCTools.get_number_of_vertices(shape2)
907 if num_verts_2 < 5e4:
908 num_faces_2 = PyOCCTools.get_number_of_faces(shape2)
909 sample_points_per_face = math.floor(math.sqrt(
910 (final_num_points - num_verts_2) / num_faces_2))
911 points_on_shape2 = PyOCCTools.sample_points_on_faces(
912 shape2, u_samples=sample_points_per_face,
913 v_samples=sample_points_per_face)
914 else:
915 points_on_shape2 = PyOCCTools.get_points_of_face(shape2)
916 points_on_shape2 = [(p.X(), p.Y(), p.Z()) for p in points_on_shape2]
918 tree1 = KDTree(points_on_shape1)
919 distances, _ = tree1.query(points_on_shape2)
920 # print(f"Minimum distance: {min(distances)}")
921 return min(distances)
923 @staticmethod
924 def create_offset_shape(shape, offset, tolerance=0.0001):
925 sewing = BRepBuilderAPI_Sewing()
926 sewing.SetTolerance(tolerance)
927 sewing.Add(shape)
928 sewing.Perform()
929 sewed_shell = sewing.SewedShape()
930 offset_builder = BRepOffsetAPI_MakeOffsetShape()
931 offset_builder.PerformBySimple(sewed_shell, offset)
932 return offset_builder.Shape()
934 @staticmethod
935 def unify_shape(shape):
936 unify = ShapeUpgrade_UnifySameDomain()
937 unify.Initialize(shape)
938 unify.Build()
939 return unify.Shape()
941 @staticmethod
942 def enlarge_bounding_box_shape_in_dir(shape, distance=0.05,
943 direction=gp_Dir(0, 0, 1)):
944 (min_box, max_box) = PyOCCTools.simple_bounding_box([shape])
945 p1 = BRepBuilderAPI_MakeVertex(gp_Pnt(*min_box)).Vertex()
946 moved_p1 = BRep_Tool.Pnt(PyOCCTools.move_bound_in_direction_of_normal(
947 p1, distance, move_dir=direction, reverse=True))
948 p2 = BRepBuilderAPI_MakeVertex(gp_Pnt(*max_box)).Vertex()
949 moved_p2 = BRep_Tool.Pnt(PyOCCTools.move_bound_in_direction_of_normal(
950 p2, distance, move_dir=direction, reverse=False))
951 new_shape = BRepPrimAPI_MakeBox(moved_p1, moved_p2).Shape()
952 return new_shape
954 @staticmethod
955 def fuse_shapes(shapes:List[TopoDS_Shape]):
956 if not shapes:
957 return None
958 if len(shapes) < 2:
959 return shapes[0]
960 fuse_shape = shapes[0]
961 for shape in shapes[1:]:
962 if not shape.IsNull():
963 fuse_shape = BRepAlgoAPI_Fuse(fuse_shape, shape).Shape()
964 return fuse_shape
966 @staticmethod
967 def get_projection_of_bounding_box(shapes: list[TopoDS_Shape],
968 proj_type: Union['x','y','z'],
969 value:float=None,
970 ) -> TopoDS_Shape:
971 ((x1, y1, z1), (x2, y2, z2)) = PyOCCTools.simple_bounding_box(shapes)
972 if proj_type == 'x':
973 if not value:
974 value = x1
975 pnt_list = [(value, y1, z1), (value, y2, z1), (value, y2, z2),
976 (value, y1, z2)]
977 elif proj_type == 'y':
978 if not value:
979 value = y1
980 pnt_list = [(x1, value, z1), (x2, value, z1), (x2, value, z2),
981 (x1, value, z2)]
982 else:
983 if not value:
984 value = z1
985 pnt_list = [(x1, y1, value), (x2, y1, value), (x2, y2, value),
986 (x1, y2, value)]
987 return PyOCCTools.make_faces_from_pnts(pnt_list)
989 @staticmethod
990 def find_min_distance_along_direction(start_point, direction, shape,
991 max_distance=1e6):
992 """Finds the minimum distance from a start point to a shape along a
993 given direction.
995 Args:
996 start_point (gp_Pnt): The starting point.
997 direction (gp_Dir): The direction vector.
998 shape (TopoDS_Shape): The target shape.
999 max_distance (float, optional): The maximum search distance.
1000 Defaults to 1e6.
1002 Returns:
1003 tuple or (None, None): A tuple containing the distance and
1004 intersection point, or (None, None) if no intersection is found.
1005 """
1006 # Create a Geom_Line (infinite, but we limit it to the maximum distance)
1007 line = Geom_Line(gp_Lin(start_point, direction))
1008 # assert line.DynamicType().Name() == "Geom_Curve"
1009 line_handle = Geom_Curve.DownCast(line)
1011 # Explore the faces of the shape
1012 explorer = TopExp_Explorer(shape, TopAbs_FACE)
1013 intersections = []
1015 while explorer.More():
1016 face = topods.Face(explorer.Current())
1017 # Get the geometry of the face
1018 geom_face = BRep_Tool.Surface(face)
1019 # assert geom_face.DynamicType().Name() == "Geom_Surface"
1020 surf_handle = Geom_Surface.DownCast(geom_face)
1022 # Calculate the intersection between the line and the face
1023 intersector = GeomAPI_IntCS(line_handle, surf_handle)
1024 intersector.Perform(line_handle, surf_handle)
1026 if intersector.IsDone():
1027 for i in range(1, intersector.NbPoints() + 1):
1028 pnt = intersector.Point(i)
1029 # Vector from the start point to the intersection point
1030 vec = gp_Pnt(start_point.X(), start_point.Y(),
1031 start_point.Z()).Distance(pnt)
1033 # Alternatively: Determine if the point lies in the desired direction
1034 # Calculate the dot product between (pnt - start_point) and direction
1035 delta = gp_Pnt(pnt.X() - start_point.X(),
1036 pnt.Y() - start_point.Y(),
1037 pnt.Z() - start_point.Z())
1039 dot = delta.X() * direction.X() + delta.Y() * direction.Y() + delta.Z() * direction.Z()
1040 if dot > 0: # Only points in the direction of the ray
1041 intersections.append((start_point.Distance(pnt), pnt))
1043 explorer.Next()
1045 if not intersections:
1046 return None, None # No intersection found
1048 # Find the intersection point with the smallest positive distance (dot)
1049 intersections.sort(key=lambda x: x[0])
1050 min_distance = intersections[0][0]# / gp_Vec(direction).Magnitude()
1051 intersection_point = intersections[0][1]
1053 # Optionally: Limit the search to max_distance
1054 if min_distance > max_distance:
1055 return None, None
1057 return min_distance, intersection_point
1059 @staticmethod
1060 def extrude_face_in_direction(shape:TopoDS_Shape, distance:float=0.1,
1061 direction:gp_Dir=gp_Dir(0,0,1),
1062 bidirectional=True):
1063 extrusion_vec = gp_Vec(
1064 direction.X()*distance, direction.Y()*distance, direction.Z()*distance
1065 )
1066 extrusion = BRepPrimAPI_MakePrism(shape, extrusion_vec).Shape()
1067 if bidirectional:
1068 extrusion_vec1 = gp_Vec(
1069 direction.X() * -distance, direction.Y() * -distance,
1070 direction.Z() * -distance
1071 )
1072 extrusion1 = BRepPrimAPI_MakePrism(shape, extrusion_vec1).Shape()
1073 extrusion = PyOCCTools.fuse_shapes([extrusion, extrusion1])
1074 return extrusion
1076 @staticmethod
1077 def sweep_line_find_intersections_multiple_shapes(p1, p2, shapes,
1078 intersection_direction,
1079 max_distance=100.0,
1080 step=0.005):
1081 """
1082 Translates a line defined by two points along its direction and finds
1083 all intersections
1084 with multiple target shapes.
1086 Args:
1087 p1 (gp_Pnt): The first point defining the original line.
1088 p2 (gp_Pnt): The second point defining the original line.
1089 shapes (list of TopoDS_Shape): The list of target shapes to intersect with.
1090 intersection_direction (gp_Dir): direction to find intersections
1091 max_distance (float, optional): The maximum translation distance. Defaults to 100.0.
1092 step (float, optional): The incremental step for translation.
1093 Defaults to 0.05.
1095 Returns:
1096 tuple:
1097 - translated_lines (list of Geom_Line): All translated lines.
1098 - intersection_points (list of tuples): Each tuple contains (t, shape_index, gp_Pnt).
1099 - min_t (float or None): The minimal translation distance where an intersection occurs.
1100 - min_delta (float or None): The minimal distance where
1101 intersection.
1102 - min_pnt (gp_Pnt or None): The corresponding intersection point at min_t.
1103 """
1104 translated_lines = []
1105 intersection_points = []
1107 # Compute the direction vector from p1 to p2
1108 direction_vec = gp_Vec(p1, p2)
1109 direction_norm = direction_vec.Magnitude()
1111 if direction_norm == 0:
1112 raise ValueError(
1113 "The two points p1 and p2 must define a valid line (distinct "
1114 "points).")
1116 # Normalize the direction vector
1117 direction_unit = gp_Dir(direction_vec)
1119 # Number of steps
1120 num_steps = math.ceil(p1.Distance(p2) / step)
1122 for i in range(num_steps + 1):
1123 t = i * step
1125 translation_vec = gp_Vec(direction_unit)
1126 translation_vec.Scale(t)
1128 translated_p1 = p1.Translated(translation_vec)
1130 # Define the translated line
1131 translated_lin = gp_Lin(translated_p1, intersection_direction)
1132 geom_translated_line = Geom_Line(translated_lin)
1133 translated_lines.append((t, geom_translated_line))
1135 # Iterate through all target shapes
1136 for shape_index, shape in enumerate(shapes):
1137 explorer = TopExp_Explorer(shape, TopAbs_FACE)
1138 while explorer.More():
1139 face = topods.Face(explorer.Current())
1140 # Get the surface geometry of the face
1141 face_surface = BRep_Tool.Surface(face)
1142 surf_handle = Geom_Surface.DownCast(face_surface)
1144 # Prepare the line for intersection
1145 line_handle = Geom_Curve.DownCast(
1146 geom_translated_line)
1148 # Compute intersection
1149 intersector = GeomAPI_IntCS(line_handle, surf_handle)
1150 intersector.Perform(line_handle, surf_handle)
1152 if intersector.IsDone():
1153 for j in range(1, intersector.NbPoints() + 1):
1154 pnt = intersector.Point(j)
1155 # Check if the intersection point is within the surface bounds
1156 if BRepExtrema_DistShapeShape(BRepBuilderAPI_MakeVertex(pnt).Vertex(), shape,
1157 Extrema_ExtFlag_MIN).Value() < 1e-6:
1158 # Calculate vector from translated_p1 to intersection point
1159 delta = gp_Vec(translated_p1, pnt)
1161 # Ensure the intersection point lies in the positive direction
1162 if delta.Dot(gp_Vec(intersection_direction)) >= 0\
1163 and translated_p1.Distance(pnt) < max_distance:
1164 # Store the intersection with t, shape index, and point
1165 intersection_points.append(
1166 (t, translated_p1.Distance(pnt),
1167 shape_index, pnt))
1168 explorer.Next()
1170 # Find the minimal translation distance
1171 if intersection_points:
1172 # Sort based on translation distance 'delta'
1173 intersection_points_sorted = sorted(intersection_points,
1174 key=lambda x: x[1])
1175 min_t, min_delta, min_shape_index, min_pnt = (
1176 intersection_points_sorted)[0]
1177 else:
1178 min_t, min_delta, min_shape_index, min_pnt = None, None, None, None
1180 return translated_lines, intersection_points, min_t, min_delta, min_pnt
1182 @staticmethod
1183 def transform_set_to_origin_based_on_surface(face_normal, compound,
1184 ref_dir,
1185 rot_ax_dir=gp_Dir(0,0,1)):
1186 """
1188 Args:
1189 face:
1190 compound:
1191 ref_dir: reference direction to compare surface normal of face
1193 Returns:
1195 """
1197 rotation_radians = gp_Vec(face_normal).AngleWithRef(ref_dir,
1198 gp_Vec(rot_ax_dir))
1200 rotation_center = PyOCCTools.get_center_of_shape(compound)
1201 rotation_axis = gp_Ax1(rotation_center, rot_ax_dir) # rotate around z
1202 trsf_rot = gp_Trsf()
1203 trsf_rot.SetRotation(rotation_axis, rotation_radians)
1204 rot_compound = BRepBuilderAPI_Transform(compound, trsf_rot).Shape()
1205 # rot_face = BRepBuilderAPI_Transform(face, trsf_rot).Shape()
1207 bbox_min, bbox_max = PyOCCTools.simple_bounding_box([rot_compound])
1209 # translate bbox_min to origin (0,0,0)
1210 trsf_trans = gp_Trsf()
1211 trsf_trans.SetTranslation(gp_Pnt(*bbox_min), gp_Pnt(0,0,0))
1212 trsf = trsf_trans.Multiplied(trsf_rot)
1214 return trsf