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