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

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 

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 

39 

40 

41class PyOCCTools: 

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

43 

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 

58 

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

65 

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 

76 

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 

97 

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

104 

105 return nb_vertex 

106 

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

113 

114 return nb_faces 

115 

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 

134 

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

144 

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

154 

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

164 

165 @staticmethod 

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

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

168 

169 Args: 

170 volume: TopoDS_Shape 

171 

172 Returns: gp_Pnt of the center of mass 

173 """ 

174 prop = GProp_GProps() 

175 brepgprop.VolumeProperties(volume, prop) 

176 return prop.CentreOfMass() 

177 

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

194 

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

211 

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: 

226 

227 Returns: 

228 

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) 

236 

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

247 

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

255 

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

264 

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 

294 

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 

309 

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 

318 

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) 

327 

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) 

336 

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 

351 

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 

357 

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 

370 

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 

381 

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 

389 

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 

409 

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. 

415 

416 Args: 

417 shape: TopoDS_Shape 

418 

419 Returns: 

420 volume: float 

421 """ 

422 props = GProp_GProps() 

423 brepgprop.VolumeProperties(shape, props) 

424 volume = props.Mass() 

425 return volume 

426 

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

435 

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. 

442 

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 

463 

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

468 

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 

473 

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 

499 

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) 

537 

538 return_shape = PyOCCTools.sew_shapes(footprint_shapes) 

539 return return_shape 

540 

541 @staticmethod 

542 def triangulate_bound_shape(shape: TopoDS_Shape, 

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

544 -> TopoDS_Shape: 

545 """Triangulate bound shape. 

546 

547 Args: 

548 shape: TopoDS_Shape 

549 cut_shapes: list of TopoDS_Shape 

550 Returns: 

551 Triangulated TopoDS_Shape 

552 

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

560 

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. 

565 

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

567 True if gp_Pnt is included, else False. 

568 

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 

573 

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) 

580 

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

582 pnt_in_solid = True 

583 return pnt_in_solid 

584 

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. 

588 

589 Args: 

590 faces: list of TopoDS_Face 

591 

592 Returns: TopoDS_Shell 

593 """ 

594 shell = BRepBuilderAPI_MakeShell() 

595 shell = shell.Shell() 

596 builder = TopoDS_Builder() 

597 builder.MakeShell(shell) 

598 

599 for face in faces: 

600 builder.Add(shell, face) 

601 return shell 

602 

603 @staticmethod 

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

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

606 

607 Args: 

608 shell: TopoDS_Shell 

609 

610 Returns: TopoDS_Solid 

611 """ 

612 solid = BRepBuilderAPI_MakeSolid() 

613 solid.Add(shell) 

614 return solid.Solid() 

615 

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

617 """Make a TopoDS_Solid from a TopoDS_Shape. 

618 

619 Args: 

620 base_shape: TopoDS_Shape 

621 

622 Returns: TopoDS_Solid 

623 

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) 

628 

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. 

632 

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. 

637 

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) 

642 

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) 

649 

650 return PyOCCTools.check_pnt_in_solid(obj1_solid, obj2_center) 

651 

652 @staticmethod 

653 def get_minimal_bounding_box(shape): 

654 # Create an empty bounding box 

655 bbox = Bnd_Box() 

656 

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

662 

663 # Get the minimal bounding box 

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

665 

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

667 

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. 

672 

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

683 

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

685 

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

691 

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 

702 

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

779 

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 

818 

819 @staticmethod 

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

821 """ 

822 

823 Args: 

824 shape: 

825 axis: 

826 rotation: 

827 

828 Returns: 

829 

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

839 

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 

844 

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 

860 

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 

890 

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] 

905 

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] 

917 

918 tree1 = KDTree(points_on_shape1) 

919 distances, _ = tree1.query(points_on_shape2) 

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

921 return min(distances) 

922 

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

933 

934 @staticmethod 

935 def unify_shape(shape): 

936 unify = ShapeUpgrade_UnifySameDomain() 

937 unify.Initialize(shape) 

938 unify.Build() 

939 return unify.Shape() 

940 

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 

953 

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 

965 

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) 

988 

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. 

994 

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. 

1001 

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) 

1010 

1011 # Explore the faces of the shape 

1012 explorer = TopExp_Explorer(shape, TopAbs_FACE) 

1013 intersections = [] 

1014 

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) 

1021 

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) 

1025 

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) 

1032 

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

1038 

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

1042 

1043 explorer.Next() 

1044 

1045 if not intersections: 

1046 return None, None # No intersection found 

1047 

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] 

1052 

1053 # Optionally: Limit the search to max_distance 

1054 if min_distance > max_distance: 

1055 return None, None 

1056 

1057 return min_distance, intersection_point 

1058 

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 

1075 

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. 

1085 

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. 

1094 

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

1106 

1107 # Compute the direction vector from p1 to p2 

1108 direction_vec = gp_Vec(p1, p2) 

1109 direction_norm = direction_vec.Magnitude() 

1110 

1111 if direction_norm == 0: 

1112 raise ValueError( 

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

1114 "points).") 

1115 

1116 # Normalize the direction vector 

1117 direction_unit = gp_Dir(direction_vec) 

1118 

1119 # Number of steps 

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

1121 

1122 for i in range(num_steps + 1): 

1123 t = i * step 

1124 

1125 translation_vec = gp_Vec(direction_unit) 

1126 translation_vec.Scale(t) 

1127 

1128 translated_p1 = p1.Translated(translation_vec) 

1129 

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

1134 

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) 

1143 

1144 # Prepare the line for intersection 

1145 line_handle = Geom_Curve.DownCast( 

1146 geom_translated_line) 

1147 

1148 # Compute intersection 

1149 intersector = GeomAPI_IntCS(line_handle, surf_handle) 

1150 intersector.Perform(line_handle, surf_handle) 

1151 

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) 

1160 

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

1169 

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 

1179 

1180 return translated_lines, intersection_points, min_t, min_delta, min_pnt 

1181 

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

1187 

1188 Args: 

1189 face: 

1190 compound: 

1191 ref_dir: reference direction to compare surface normal of face 

1192 

1193 Returns: 

1194 

1195 """ 

1196 

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

1198 gp_Vec(rot_ax_dir)) 

1199 

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

1206 

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

1208 

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) 

1213 

1214 return trsf