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

1""" 

2Common tools for handling OCC Shapes within the bim2sim project. 

3""" 

4import math 

5from typing import List, Tuple, Union 

6 

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 

40 

41 

42class PyOCCTools: 

43 """Class for Tools handling and modifying Python OCC Shapes""" 

44 

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 

59 

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 = [] 

66 

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 

77 

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 

98 

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() 

105 

106 return nb_vertex 

107 

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() 

114 

115 return nb_faces 

116 

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 

135 

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() 

145 

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() 

155 

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() 

165 

166 @staticmethod 

167 def get_center_of_volume(volume: TopoDS_Shape) -> gp_Pnt: 

168 """Compute the center of mass of a TopoDS_Shape volume. 

169 

170 Args: 

171 volume: TopoDS_Shape 

172 

173 Returns: gp_Pnt of the center of mass 

174 """ 

175 prop = GProp_GProps() 

176 brepgprop_VolumeProperties(volume, prop) 

177 return prop.CentreOfMass() 

178 

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() 

195 

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() 

212 

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: 

227 

228 Returns: 

229 

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) 

237 

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() 

248 

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() 

256 

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() 

265 

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 

295 

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 

310 

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 

319 

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) 

328 

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) 

337 

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 

352 

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 

358 

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 

371 

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 

382 

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 

390 

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 

410 

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. 

416 

417 Args: 

418 shape: TopoDS_Shape 

419 

420 Returns: 

421 volume: float 

422 """ 

423 props = GProp_GProps() 

424 brepgprop_VolumeProperties(shape, props) 

425 volume = props.Mass() 

426 return volume 

427 

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() 

436 

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. 

443 

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 

464 

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]]: 

469 

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 

474 

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 

500 

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) 

538 

539 return_shape = PyOCCTools.sew_shapes(footprint_shapes) 

540 return return_shape 

541 

542 @staticmethod 

543 def triangulate_bound_shape(shape: TopoDS_Shape, 

544 cut_shapes: list[TopoDS_Shape] = []) \ 

545 -> TopoDS_Shape: 

546 """Triangulate bound shape. 

547 

548 Args: 

549 shape: TopoDS_Shape 

550 cut_shapes: list of TopoDS_Shape 

551 Returns: 

552 Triangulated TopoDS_Shape 

553 

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() 

561 

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. 

566 

567 This method checks if a gp_Pnt is included in a TopoDS_Solid. Returns 

568 True if gp_Pnt is included, else False. 

569 

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 

574 

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) 

581 

582 if not classifier.State() == TopAbs_OUT: # check if center is in solid 

583 pnt_in_solid = True 

584 return pnt_in_solid 

585 

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. 

589 

590 Args: 

591 faces: list of TopoDS_Face 

592 

593 Returns: TopoDS_Shell 

594 """ 

595 shell = BRepBuilderAPI_MakeShell() 

596 shell = shell.Shell() 

597 builder = TopoDS_Builder() 

598 builder.MakeShell(shell) 

599 

600 for face in faces: 

601 builder.Add(shell, face) 

602 return shell 

603 

604 @staticmethod 

605 def make_solid_from_shell(shell: TopoDS_Shell) -> TopoDS_Solid: 

606 """Create a TopoDS_Solid from a given TopoDS_Shell. 

607 

608 Args: 

609 shell: TopoDS_Shell 

610 

611 Returns: TopoDS_Solid 

612 """ 

613 solid = BRepBuilderAPI_MakeSolid() 

614 solid.Add(shell) 

615 return solid.Solid() 

616 

617 def make_solid_from_shape(self, base_shape: TopoDS_Shape) -> TopoDS_Solid: 

618 """Make a TopoDS_Solid from a TopoDS_Shape. 

619 

620 Args: 

621 base_shape: TopoDS_Shape 

622 

623 Returns: TopoDS_Solid 

624 

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) 

629 

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. 

633 

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. 

638 

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) 

643 

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) 

650 

651 return PyOCCTools.check_pnt_in_solid(obj1_solid, obj2_center) 

652 

653 @staticmethod 

654 def get_minimal_bounding_box(shape): 

655 # Create an empty bounding box 

656 bbox = Bnd_Box() 

657 

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() 

663 

664 # Get the minimal bounding box 

665 min_x, min_y, min_z, max_x, max_y, max_z = bbox.Get() 

666 

667 return (min_x, min_y, min_z), (max_x, max_y, max_z) 

668 

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. 

673 

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() 

684 

685 return (min_x, min_y, min_z), (max_x, max_y, max_z) 

686 

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() 

692 

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 

703 

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])]) 

780 

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 

819 

820 @staticmethod 

821 def rotate_by_deg(shape, axis='z', rotation=90): 

822 """ 

823 

824 Args: 

825 shape: 

826 axis: 

827 rotation: 

828 

829 Returns: 

830 

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)) 

840 

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 

845 

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 

861 

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 

891 

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] 

906 

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] 

918 

919 tree1 = KDTree(points_on_shape1) 

920 distances, _ = tree1.query(points_on_shape2) 

921 # print(f"Minimum distance: {min(distances)}") 

922 return min(distances) 

923 

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() 

934 

935 @staticmethod 

936 def unify_shape(shape): 

937 unify = ShapeUpgrade_UnifySameDomain() 

938 unify.Initialize(shape) 

939 unify.Build() 

940 return unify.Shape() 

941 

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 

954 

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 

966 

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) 

989 

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. 

995 

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. 

1002 

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) 

1011 

1012 # Explore the faces of the shape 

1013 explorer = TopExp_Explorer(shape, TopAbs_FACE) 

1014 intersections = [] 

1015 

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) 

1022 

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) 

1026 

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) 

1033 

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()) 

1039 

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)) 

1043 

1044 explorer.Next() 

1045 

1046 if not intersections: 

1047 return None, None # No intersection found 

1048 

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] 

1053 

1054 # Optionally: Limit the search to max_distance 

1055 if min_distance > max_distance: 

1056 return None, None 

1057 

1058 return min_distance, intersection_point 

1059 

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 

1076 

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. 

1086 

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. 

1095 

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 = [] 

1107 

1108 # Compute the direction vector from p1 to p2 

1109 direction_vec = gp_Vec(p1, p2) 

1110 direction_norm = direction_vec.Magnitude() 

1111 

1112 if direction_norm == 0: 

1113 raise ValueError( 

1114 "The two points p1 and p2 must define a valid line (distinct " 

1115 "points).") 

1116 

1117 # Normalize the direction vector 

1118 direction_unit = gp_Dir(direction_vec) 

1119 

1120 # Number of steps 

1121 num_steps = math.ceil(p1.Distance(p2) / step) 

1122 

1123 for i in range(num_steps + 1): 

1124 t = i * step 

1125 

1126 translation_vec = gp_Vec(direction_unit) 

1127 translation_vec.Scale(t) 

1128 

1129 translated_p1 = p1.Translated(translation_vec) 

1130 

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)) 

1135 

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) 

1144 

1145 # Prepare the line for intersection 

1146 line_handle = Handle_Geom_Curve_DownCast( 

1147 geom_translated_line) 

1148 

1149 # Compute intersection 

1150 intersector = GeomAPI_IntCS(line_handle, surf_handle) 

1151 intersector.Perform(line_handle, surf_handle) 

1152 

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) 

1161 

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() 

1170 

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 

1180 

1181 return translated_lines, intersection_points, min_t, min_delta, min_pnt 

1182 

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 """ 

1188 

1189 Args: 

1190 face: 

1191 compound: 

1192 ref_dir: reference direction to compare surface normal of face 

1193 

1194 Returns: 

1195 

1196 """ 

1197 

1198 rotation_radians = gp_Vec(face_normal).AngleWithRef(ref_dir, 

1199 gp_Vec(rot_ax_dir)) 

1200 

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() 

1207 

1208 bbox_min, bbox_max = PyOCCTools.simple_bounding_box([rot_compound]) 

1209 

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) 

1214 

1215 return trsf