Coverage for bim2sim/utilities/pyocc_tools.py: 50%

335 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-12 13:34 +0000

1""" 

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

3""" 

4from typing import List, Tuple, Union 

5 

6import numpy as np 

7from OCC.Core.BRep import BRep_Tool 

8from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut 

9from OCC.Core.BRepBndLib import brepbndlib_Add 

10from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, \ 

11 BRepBuilderAPI_Transform, BRepBuilderAPI_MakePolygon, \ 

12 BRepBuilderAPI_MakeShell, BRepBuilderAPI_MakeSolid, BRepBuilderAPI_Sewing 

13from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier 

14from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape 

15from OCC.Core.BRepGProp import brepgprop_SurfaceProperties, \ 

16 brepgprop_LinearProperties, brepgprop_VolumeProperties, BRepGProp_Face 

17from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh 

18from OCC.Core.BRepTools import BRepTools_WireExplorer 

19from OCC.Core.Bnd import Bnd_Box 

20from OCC.Core.Extrema import Extrema_ExtFlag_MIN 

21from OCC.Core.GProp import GProp_GProps 

22from OCC.Core.Geom import Handle_Geom_Plane_DownCast 

23from OCC.Core.ShapeAnalysis import ShapeAnalysis_ShapeContents 

24from OCC.Core.ShapeFix import ShapeFix_Face, ShapeFix_Shape 

25from OCC.Core.TopAbs import TopAbs_WIRE, TopAbs_FACE, TopAbs_OUT 

26from OCC.Core.TopExp import TopExp_Explorer 

27from OCC.Core.TopoDS import topods_Wire, TopoDS_Face, TopoDS_Shape, \ 

28 topods_Face, TopoDS_Edge, TopoDS_Solid, TopoDS_Shell, TopoDS_Builder 

29from OCC.Core.gp import gp_XYZ, gp_Pnt, gp_Trsf, gp_Vec 

30 

31 

32class PyOCCTools: 

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

34 

35 @staticmethod 

36 def remove_coincident_vertices(vert_list: List[gp_Pnt]) -> List[gp_Pnt]: 

37 """ remove coincident vertices from list of gp_Pnt. 

38 Vertices are coincident if closer than tolerance.""" 

39 tol_dist = 1e-2 

40 new_list = [] 

41 v_b = np.array(vert_list[-1].Coord()) 

42 for vert in vert_list: 

43 v = np.array(vert.Coord()) 

44 d_b = np.linalg.norm(v - v_b) 

45 if d_b > tol_dist: 

46 new_list.append(vert) 

47 v_b = v 

48 return new_list 

49 

50 @staticmethod 

51 def remove_collinear_vertices2(vert_list: List[gp_Pnt]) -> List[gp_Pnt]: 

52 """ remove collinear vertices from list of gp_Pnt. 

53 Vertices are collinear if cross product less tolerance.""" 

54 tol_cross = 1e-3 

55 new_list = [] 

56 

57 for i, vert in enumerate(vert_list): 

58 v = np.array(vert.Coord()) 

59 v_b = np.array(vert_list[(i - 1) % (len(vert_list))].Coord()) 

60 v_f = np.array(vert_list[(i + 1) % (len(vert_list))].Coord()) 

61 v1 = v - v_b 

62 v2 = v_f - v_b 

63 if np.linalg.norm(np.cross(v1, v2)) / np.linalg.norm( 

64 v2) > tol_cross: 

65 new_list.append(vert) 

66 return new_list 

67 

68 @staticmethod 

69 def make_faces_from_pnts( 

70 pnt_list: Union[List[Tuple[float]], List[gp_Pnt]]) -> TopoDS_Face: 

71 """ 

72 This function returns a TopoDS_Face from list of gp_Pnt 

73 :param pnt_list: list of gp_Pnt or Coordinate-Tuples 

74 :return: TopoDS_Face 

75 """ 

76 if isinstance(pnt_list[0], tuple): 

77 new_list = [] 

78 for pnt in pnt_list: 

79 new_list.append(gp_Pnt(gp_XYZ(pnt[0], pnt[1], pnt[2]))) 

80 pnt_list = new_list 

81 poly = BRepBuilderAPI_MakePolygon() 

82 for coord in pnt_list: 

83 poly.Add(coord) 

84 poly.Close() 

85 a_wire = poly.Wire() 

86 a_face = BRepBuilderAPI_MakeFace(a_wire).Face() 

87 return a_face 

88 

89 @staticmethod 

90 def get_number_of_vertices(shape: TopoDS_Shape) -> int: 

91 """ get number of vertices of a shape""" 

92 shape_analysis = ShapeAnalysis_ShapeContents() 

93 shape_analysis.Perform(shape) 

94 nb_vertex = shape_analysis.NbVertices() 

95 

96 return nb_vertex 

97 

98 @staticmethod 

99 def get_points_of_face(shape: TopoDS_Shape) -> List[gp_Pnt]: 

100 """ 

101 This function returns a list of gp_Pnt of a Surface 

102 :param shape: TopoDS_Shape (Surface) 

103 :return: pnt_list (list of gp_Pnt) 

104 """ 

105 an_exp = TopExp_Explorer(shape, TopAbs_WIRE) 

106 pnt_list = [] 

107 while an_exp.More(): 

108 wire = topods_Wire(an_exp.Current()) 

109 w_exp = BRepTools_WireExplorer(wire) 

110 while w_exp.More(): 

111 pnt1 = BRep_Tool.Pnt(w_exp.CurrentVertex()) 

112 pnt_list.append(pnt1) 

113 w_exp.Next() 

114 an_exp.Next() 

115 return pnt_list 

116 

117 @staticmethod 

118 def get_center_of_face(face: TopoDS_Face) -> gp_Pnt: 

119 """ 

120 Calculates the center of the given face. The center point is the center 

121 of mass. 

122 """ 

123 prop = GProp_GProps() 

124 brepgprop_SurfaceProperties(face, prop) 

125 return prop.CentreOfMass() 

126 

127 @staticmethod 

128 def get_center_of_shape(shape: TopoDS_Shape) -> gp_Pnt: 

129 """ 

130 Calculates the center of the given shape. The center point is the 

131 center of mass. 

132 """ 

133 prop = GProp_GProps() 

134 brepgprop_VolumeProperties(shape, prop) 

135 return prop.CentreOfMass() 

136 

137 @staticmethod 

138 def get_center_of_edge(edge): 

139 """ 

140 Calculates the center of the given edge. The center point is the center 

141 of mass. 

142 """ 

143 prop = GProp_GProps() 

144 brepgprop_LinearProperties(edge, prop) 

145 return prop.CentreOfMass() 

146 

147 @staticmethod 

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

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

150 

151 Args: 

152 volume: TopoDS_Shape 

153 

154 Returns: gp_Pnt of the center of mass 

155 """ 

156 prop = GProp_GProps() 

157 brepgprop_VolumeProperties(volume, prop) 

158 return prop.CentreOfMass() 

159 

160 @staticmethod 

161 def scale_face(face: TopoDS_Face, factor: float, 

162 predefined_center: gp_Pnt = None) -> TopoDS_Shape: 

163 """ 

164 Scales the given face by the given factor, using the center of mass of 

165 the face as origin of the transformation. If another center than the 

166 center of mass should be used for the origin of the transformation, 

167 set the predefined_center. 

168 """ 

169 if not predefined_center: 

170 center = PyOCCTools.get_center_of_face(face) 

171 else: 

172 center = predefined_center 

173 trsf = gp_Trsf() 

174 trsf.SetScale(center, factor) 

175 return BRepBuilderAPI_Transform(face, trsf).Shape() 

176 

177 @staticmethod 

178 def scale_shape(shape: TopoDS_Shape, factor: float, 

179 predefined_center: gp_Pnt = None) -> TopoDS_Shape: 

180 """ 

181 Scales the given shape by the given factor, using the center of mass of 

182 the shape as origin of the transformation. If another center than the 

183 center of mass should be used for the origin of the transformation, 

184 set the predefined_center. 

185 """ 

186 if not predefined_center: 

187 center = PyOCCTools.get_center_of_volume(shape) 

188 else: 

189 center = predefined_center 

190 trsf = gp_Trsf() 

191 trsf.SetScale(center, factor) 

192 return BRepBuilderAPI_Transform(shape, trsf).Shape() 

193 

194 @staticmethod 

195 def scale_edge(edge: TopoDS_Edge, factor: float) -> TopoDS_Shape: 

196 """ 

197 Scales the given edge by the given factor, using the center of mass of 

198 the edge as origin of the transformation. 

199 """ 

200 center = PyOCCTools.get_center_of_edge(edge) 

201 trsf = gp_Trsf() 

202 trsf.SetScale(center, factor) 

203 return BRepBuilderAPI_Transform(edge, trsf).Shape() 

204 

205 @staticmethod 

206 def fix_face(face: TopoDS_Face, tolerance=1e-3) -> TopoDS_Face: 

207 """Apply shape healing on a face.""" 

208 fix = ShapeFix_Face(face) 

209 fix.SetMaxTolerance(tolerance) 

210 fix.Perform() 

211 return fix.Face() 

212 

213 @staticmethod 

214 def fix_shape(shape: TopoDS_Shape, tolerance=1e-3) -> TopoDS_Shape: 

215 """Apply shape healing on a shape.""" 

216 fix = ShapeFix_Shape(shape) 

217 fix.SetFixFreeShellMode(True) 

218 fix.LimitTolerance(tolerance) 

219 fix.Perform() 

220 return fix.Shape() 

221 

222 @staticmethod 

223 def move_bound_in_direction_of_normal(bound, move_dist: float, 

224 reverse=False) -> TopoDS_Shape: 

225 """Move a BIM2SIM Space Boundary in the direction of its surface 

226 normal by a given distance.""" 

227 if isinstance(bound, TopoDS_Shape): 

228 bound_normal = PyOCCTools.simple_face_normal(bound) 

229 bound_shape = bound 

230 else: 

231 bound_normal = bound.bound_normal 

232 bound_shape = bound.bound_shape 

233 prod_vec = [] 

234 move_dir = bound_normal.Coord() 

235 if reverse: 

236 move_dir = bound_normal.Reversed().Coord() 

237 for i in move_dir: 

238 prod_vec.append(move_dist * i) 

239 # move bound in direction of bound normal by move_dist 

240 trsf = gp_Trsf() 

241 coord = gp_XYZ(*prod_vec) 

242 vec = gp_Vec(coord) 

243 trsf.SetTranslation(vec) 

244 new_shape = BRepBuilderAPI_Transform(bound_shape, trsf).Shape() 

245 return new_shape 

246 

247 @staticmethod 

248 def compare_direction_of_normals(normal1: gp_XYZ, normal2: gp_XYZ) -> bool: 

249 """ 

250 Compare the direction of two surface normals (vectors). 

251 True, if direction is same or reversed 

252 :param normal1: first normal (gp_Pnt) 

253 :param normal2: second normal (gp_Pnt) 

254 :return: True/False 

255 """ 

256 dotp = normal1.Dot(normal2) 

257 check = False 

258 if 1 - 1e-2 < dotp ** 2 < 1 + 1e-2: 

259 check = True 

260 return check 

261 

262 @staticmethod 

263 def _a2p(o, z, x): 

264 """Compute Axis of Local Placement of an IfcProducts Objectplacement""" 

265 y = np.cross(z, x) 

266 r = np.eye(4) 

267 r[:-1, :-1] = x, y, z 

268 r[-1, :-1] = o 

269 return r.T 

270 

271 @staticmethod 

272 def _axis2placement(plc): 

273 """Get Axis of Local Placement of an IfcProducts Objectplacement""" 

274 z = np.array(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1)) 

275 x = np.array( 

276 plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0)) 

277 o = plc.Location.Coordinates 

278 return PyOCCTools._a2p(o, z, x) 

279 

280 @staticmethod 

281 def local_placement(plc): 

282 """Get Local Placement of an IfcProducts Objectplacement""" 

283 if plc.PlacementRelTo is None: 

284 parent = np.eye(4) 

285 else: 

286 parent = PyOCCTools.local_placement(plc.PlacementRelTo) 

287 return np.dot(PyOCCTools._axis2placement(plc.RelativePlacement), parent) 

288 

289 @staticmethod 

290 def simple_face_normal(face: TopoDS_Face, check_orientation: bool = True) \ 

291 -> gp_XYZ: 

292 """Compute the normal of a TopoDS_Face.""" 

293 face = PyOCCTools.get_face_from_shape(face) 

294 surf = BRep_Tool.Surface(face) 

295 obj = surf 

296 assert obj.DynamicType().Name() == "Geom_Plane" 

297 plane = Handle_Geom_Plane_DownCast(surf) 

298 face_normal = plane.Axis().Direction().XYZ() 

299 if check_orientation: 

300 if face.Orientation() == 1: 

301 face_normal = face_normal.Reversed() 

302 return face_normal 

303 

304 @staticmethod 

305 def flip_orientation_of_face(face: TopoDS_Face) -> TopoDS_Face: 

306 """Flip the orientation of a TopoDS_Face.""" 

307 face = face.Reversed() 

308 return face 

309 

310 @staticmethod 

311 def get_face_from_shape(shape: TopoDS_Shape) -> TopoDS_Face: 

312 """Return first face of a TopoDS_Shape.""" 

313 exp = TopExp_Explorer(shape, TopAbs_FACE) 

314 face = exp.Current() 

315 try: 

316 face = topods_Face(face) 

317 except: 

318 exp1 = TopExp_Explorer(shape, TopAbs_WIRE) 

319 wire = exp1.Current() 

320 face = BRepBuilderAPI_MakeFace(wire).Face() 

321 return face 

322 

323 @staticmethod 

324 def get_faces_from_shape(shape: TopoDS_Shape) -> List[TopoDS_Face]: 

325 """Return all faces from a shape.""" 

326 faces = [] 

327 an_exp = TopExp_Explorer(shape, TopAbs_FACE) 

328 while an_exp.More(): 

329 face = topods_Face(an_exp.Current()) 

330 faces.append(face) 

331 an_exp.Next() 

332 return faces 

333 

334 @staticmethod 

335 def get_shape_area(shape: TopoDS_Shape) -> float: 

336 """compute area of a space boundary""" 

337 bound_prop = GProp_GProps() 

338 brepgprop_SurfaceProperties(shape, bound_prop) 

339 area = bound_prop.Mass() 

340 return area 

341 

342 @staticmethod 

343 def remove_coincident_and_collinear_points_from_face( 

344 face: TopoDS_Face) -> TopoDS_Face: 

345 """ 

346 removes collinear and coincident vertices iff resulting number of 

347 vertices is > 3, so a valid face can be build. 

348 """ 

349 org_area = PyOCCTools.get_shape_area(face) 

350 pnt_list = PyOCCTools.get_points_of_face(face) 

351 pnt_list_new = PyOCCTools.remove_coincident_vertices(pnt_list) 

352 pnt_list_new = PyOCCTools.remove_collinear_vertices2(pnt_list_new) 

353 if pnt_list_new != pnt_list: 

354 if len(pnt_list_new) < 3: 

355 pnt_list_new = pnt_list 

356 new_face = PyOCCTools.make_faces_from_pnts(pnt_list_new) 

357 new_area = (PyOCCTools.get_shape_area(new_face)) 

358 if abs(new_area - org_area) < 5e-3: 

359 face = new_face 

360 return face 

361 

362 @staticmethod 

363 def get_shape_volume(shape: TopoDS_Shape) -> float: 

364 """ 

365 This function computes the volume of a shape and returns the value as a 

366 float. 

367 

368 Args: 

369 shape: TopoDS_Shape 

370 

371 Returns: 

372 volume: float 

373 """ 

374 props = GProp_GProps() 

375 brepgprop_VolumeProperties(shape, props) 

376 volume = props.Mass() 

377 return volume 

378 

379 @staticmethod 

380 def sew_shapes(shape_list: list[TopoDS_Shape]) -> TopoDS_Shape: 

381 sew = BRepBuilderAPI_Sewing(0.0001) 

382 for shp in shape_list: 

383 sew.Add(shp) 

384 sew.Perform() 

385 return sew.SewedShape() 

386 

387 @staticmethod 

388 def move_bounds_to_vertical_pos(bound_list: list(), 

389 base_face: TopoDS_Face) -> list[TopoDS_Shape]: 

390 new_shape_list = [] 

391 for bound in bound_list: 

392 if not isinstance(bound, TopoDS_Shape): 

393 bound_shape = bound.bound_shape 

394 else: 

395 bound_shape = bound 

396 distance = BRepExtrema_DistShapeShape(base_face, 

397 bound_shape, 

398 Extrema_ExtFlag_MIN).Value() 

399 if abs(distance) > 1e-4: 

400 new_shape = PyOCCTools.move_bound_in_direction_of_normal( 

401 bound, distance) 

402 if abs(BRepExtrema_DistShapeShape( 

403 base_face, new_shape, Extrema_ExtFlag_MIN).Value()) \ 

404 > 1e-4: 

405 new_shape = PyOCCTools.move_bound_in_direction_of_normal( 

406 bound, distance, reverse=True) 

407 else: 

408 new_shape = bound_shape 

409 new_shape_list.append(new_shape) 

410 return new_shape_list 

411 

412 @staticmethod 

413 def get_footprint_of_shape(shape: TopoDS_Shape) -> TopoDS_Face: 

414 """ 

415 Calculate the footprint of a TopoDS_Shape. 

416 """ 

417 footprint_shapes = [] 

418 return_shape = None 

419 faces = PyOCCTools.get_faces_from_shape(shape) 

420 for face in faces: 

421 prop = BRepGProp_Face(face) 

422 p = gp_Pnt() 

423 normal_direction = gp_Vec() 

424 prop.Normal(0., 0., p, normal_direction) 

425 if abs(1 - normal_direction.Z()) < 1e-4: 

426 footprint_shapes.append(face) 

427 if len(footprint_shapes) == 0: 

428 for face in faces: 

429 prop = BRepGProp_Face(face) 

430 p = gp_Pnt() 

431 normal_direction = gp_Vec() 

432 prop.Normal(0., 0., p, normal_direction) 

433 if abs(1 - abs(normal_direction.Z())) < 1e-4: 

434 footprint_shapes.append(face) 

435 if len(footprint_shapes) == 1: 

436 return_shape = footprint_shapes[0] 

437 elif len(footprint_shapes) > 1: 

438 bbox = Bnd_Box() 

439 brepbndlib_Add(shape, bbox) 

440 xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() 

441 bbox_ground_face = PyOCCTools.make_faces_from_pnts( 

442 [(xmin, ymin, zmin), 

443 (xmin, ymax, zmin), 

444 (xmax, ymax, zmin), 

445 (xmax, ymin, zmin)] 

446 ) 

447 footprint_shapes = PyOCCTools.move_bounds_to_vertical_pos( 

448 footprint_shapes, bbox_ground_face) 

449 

450 return_shape = PyOCCTools.sew_shapes(footprint_shapes) 

451 return return_shape 

452 

453 @staticmethod 

454 def triangulate_bound_shape(shape: TopoDS_Shape, 

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

456 -> TopoDS_Shape: 

457 """Triangulate bound shape. 

458 

459 Args: 

460 shape: TopoDS_Shape 

461 cut_shapes: list of TopoDS_Shape 

462 Returns: 

463 Triangulated TopoDS_Shape 

464 

465 """ 

466 if cut_shapes: 

467 for cut_shape in cut_shapes: 

468 shape = BRepAlgoAPI_Cut( 

469 shape, cut_shape).Shape() 

470 triang_face = BRepMesh_IncrementalMesh(shape, 1) 

471 return triang_face.Shape() 

472 

473 @staticmethod 

474 def check_pnt_in_solid(solid: TopoDS_Solid, pnt: gp_Pnt, tol=1.0e-6) \ 

475 -> bool: 

476 """Check if a gp_Pnt is inside a TopoDS_Solid. 

477 

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

479 True if gp_Pnt is included, else False. 

480 

481 Args: 

482 solid: TopoDS_Solid where the gp_Pnt should be included 

483 pnt: gp_Pnt that is tested 

484 tol: tolerance, default is set to 1e-6 

485 

486 Returns: True if gp_Pnt is included in TopoDS_Solid, else False 

487 """ 

488 pnt_in_solid = False 

489 classifier = BRepClass3d_SolidClassifier() 

490 classifier.Load(solid) 

491 classifier.Perform(pnt, tol) 

492 

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

494 pnt_in_solid = True 

495 return pnt_in_solid 

496 

497 @staticmethod 

498 def make_shell_from_faces(faces: list[TopoDS_Face]) -> TopoDS_Shell: 

499 """Creates a TopoDS_Shell from a list of TopoDS_Face. 

500 

501 Args: 

502 faces: list of TopoDS_Face 

503 

504 Returns: TopoDS_Shell 

505 """ 

506 shell = BRepBuilderAPI_MakeShell() 

507 shell = shell.Shell() 

508 builder = TopoDS_Builder() 

509 builder.MakeShell(shell) 

510 

511 for face in faces: 

512 builder.Add(shell, face) 

513 return shell 

514 

515 @staticmethod 

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

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

518 

519 Args: 

520 shell: TopoDS_Shell 

521 

522 Returns: TopoDS_Solid 

523 """ 

524 solid = BRepBuilderAPI_MakeSolid() 

525 solid.Add(shell) 

526 return solid.Solid() 

527 

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

529 """Make a TopoDS_Solid from a TopoDS_Shape. 

530 

531 Args: 

532 base_shape: TopoDS_Shape 

533 

534 Returns: TopoDS_Solid 

535 

536 """ 

537 faces = self.get_faces_from_shape(base_shape) 

538 shell = self.make_shell_from_faces(faces) 

539 return self.make_solid_from_shell(shell) 

540 

541 @staticmethod 

542 def obj2_in_obj1(obj1: TopoDS_Shape, obj2: TopoDS_Shape) -> bool: 

543 """ Checks if the center of obj2 is actually in the shape of obj1. 

544 

545 This method is used to compute if the center of mass of a TopoDS_Shape 

546 is included in another TopoDS_Shape. This can be used to determine, 

547 if a HVAC element (e.g., IfcSpaceHeater) is included in the 

548 TopoDS_Shape of an IfcSpace. 

549 

550 Args: 

551 obj1: TopoDS_Shape of the larger element (e.g., IfcSpace) 

552 obj2: TopoDS_Shape of the smaller element (e.g., IfcSpaceHeater, 

553 IfcAirTerminal) 

554 

555 Returns: True if obj2 is in obj1, else False 

556 """ 

557 faces = PyOCCTools.get_faces_from_shape(obj1) 

558 shell = PyOCCTools.make_shell_from_faces(faces) 

559 obj1_solid = PyOCCTools.make_solid_from_shell(shell) 

560 obj2_center = PyOCCTools.get_center_of_volume(obj2) 

561 

562 return PyOCCTools.check_pnt_in_solid(obj1_solid, obj2_center)