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

335 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 17:09 +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 Args: 

368 shape: TopoDS_Shape 

369 Returns: 

370 volume: float 

371 """ 

372 props = GProp_GProps() 

373 brepgprop_VolumeProperties(shape, props) 

374 volume = props.Mass() 

375 return volume 

376 

377 @staticmethod 

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

379 sew = BRepBuilderAPI_Sewing(0.0001) 

380 for shp in shape_list: 

381 sew.Add(shp) 

382 sew.Perform() 

383 return sew.SewedShape() 

384 

385 @staticmethod 

386 def move_bounds_to_vertical_pos(bound_list: list(), 

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

388 new_shape_list = [] 

389 for bound in bound_list: 

390 if not isinstance(bound, TopoDS_Shape): 

391 bound_shape = bound.bound_shape 

392 else: 

393 bound_shape = bound 

394 distance = BRepExtrema_DistShapeShape(base_face, 

395 bound_shape, 

396 Extrema_ExtFlag_MIN).Value() 

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

398 new_shape = PyOCCTools.move_bound_in_direction_of_normal( 

399 bound, distance) 

400 if abs(BRepExtrema_DistShapeShape( 

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

402 > 1e-4: 

403 new_shape = PyOCCTools.move_bound_in_direction_of_normal( 

404 bound, distance, reverse=True) 

405 else: 

406 new_shape = bound_shape 

407 new_shape_list.append(new_shape) 

408 return new_shape_list 

409 

410 @staticmethod 

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

412 """ 

413 Calculate the footprint of a TopoDS_Shape. 

414 """ 

415 footprint_shapes = [] 

416 return_shape = None 

417 faces = PyOCCTools.get_faces_from_shape(shape) 

418 for face in faces: 

419 prop = BRepGProp_Face(face) 

420 p = gp_Pnt() 

421 normal_direction = gp_Vec() 

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

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

424 footprint_shapes.append(face) 

425 if len(footprint_shapes) == 0: 

426 for face in faces: 

427 prop = BRepGProp_Face(face) 

428 p = gp_Pnt() 

429 normal_direction = gp_Vec() 

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

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

432 footprint_shapes.append(face) 

433 if len(footprint_shapes) == 1: 

434 return_shape = footprint_shapes[0] 

435 elif len(footprint_shapes) > 1: 

436 bbox = Bnd_Box() 

437 brepbndlib_Add(shape, bbox) 

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

439 bbox_ground_face = PyOCCTools.make_faces_from_pnts( 

440 [(xmin, ymin, zmin), 

441 (xmin, ymax, zmin), 

442 (xmax, ymax, zmin), 

443 (xmax, ymin, zmin)] 

444 ) 

445 footprint_shapes = PyOCCTools.move_bounds_to_vertical_pos( 

446 footprint_shapes, bbox_ground_face) 

447 

448 return_shape = PyOCCTools.sew_shapes(footprint_shapes) 

449 return return_shape 

450 

451 @staticmethod 

452 def triangulate_bound_shape(shape: TopoDS_Shape, 

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

454 -> TopoDS_Shape: 

455 """Triangulate bound shape. 

456 

457 Args: 

458 shape: TopoDS_Shape 

459 cut_shapes: list of TopoDS_Shape 

460 Returns: 

461 Triangulated TopoDS_Shape 

462 

463 """ 

464 if cut_shapes: 

465 for cut_shape in cut_shapes: 

466 shape = BRepAlgoAPI_Cut( 

467 shape, cut_shape).Shape() 

468 triang_face = BRepMesh_IncrementalMesh(shape, 1) 

469 return triang_face.Shape() 

470 

471 @staticmethod 

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

473 -> bool: 

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

475 

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

477 True if gp_Pnt is included, else False. 

478 

479 Args: 

480 solid: TopoDS_Solid where the gp_Pnt should be included 

481 pnt: gp_Pnt that is tested 

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

483 

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

485 """ 

486 pnt_in_solid = False 

487 classifier = BRepClass3d_SolidClassifier() 

488 classifier.Load(solid) 

489 classifier.Perform(pnt, tol) 

490 

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

492 pnt_in_solid = True 

493 return pnt_in_solid 

494 

495 @staticmethod 

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

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

498 

499 Args: 

500 faces: list of TopoDS_Face 

501 

502 Returns: TopoDS_Shell 

503 """ 

504 shell = BRepBuilderAPI_MakeShell() 

505 shell = shell.Shell() 

506 builder = TopoDS_Builder() 

507 builder.MakeShell(shell) 

508 

509 for face in faces: 

510 builder.Add(shell, face) 

511 return shell 

512 

513 @staticmethod 

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

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

516 

517 Args: 

518 shell: TopoDS_Shell 

519 

520 Returns: TopoDS_Solid 

521 """ 

522 solid = BRepBuilderAPI_MakeSolid() 

523 solid.Add(shell) 

524 return solid.Solid() 

525 

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

527 """Make a TopoDS_Solid from a TopoDS_Shape. 

528 

529 Args: 

530 base_shape: TopoDS_Shape 

531 

532 Returns: TopoDS_Solid 

533 

534 """ 

535 faces = self.get_faces_from_shape(base_shape) 

536 shell = self.make_shell_from_faces(faces) 

537 return self.make_solid_from_shell(shell) 

538 

539 @staticmethod 

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

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

542 

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

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

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

546 TopoDS_Shape of an IfcSpace. 

547 

548 Args: 

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

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

551 IfcAirTerminal) 

552 

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

554 """ 

555 faces = PyOCCTools.get_faces_from_shape(obj1) 

556 shell = PyOCCTools.make_shell_from_faces(faces) 

557 obj1_solid = PyOCCTools.make_solid_from_shell(shell) 

558 obj2_center = PyOCCTools.get_center_of_volume(obj2) 

559 

560 return PyOCCTools.check_pnt_in_solid(obj1_solid, obj2_center)