Coverage for bim2sim/tasks/common/inner_loop_remover.py: 38%

386 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 17:09 +0000

1""" 

2This module contains functions which, given a TopoDS shapes with holes ("inner 

3loops"), calculate an equivalent shape without holes by adding cuts along 

4triangulation edges. Using the triangulation as a graph, it finds a spanning 

5tree between the main polygon and its holes using Kruskal's algorithm 

6and places the cuts along the edges of this spanning tree. 

7""" 

8 

9import logging 

10import math 

11from collections import defaultdict 

12from typing import Tuple, List, Mapping, TypeVar, Generic, Optional 

13 

14import numpy 

15# Type aliases that are used throughout this module 

16from OCC.Core.BRep import BRep_Tool 

17from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut 

18from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeEdge 

19from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape 

20from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh 

21from OCC.Core.Extrema import Extrema_ExtFlag_MIN 

22from OCC.Core.TopAbs import TopAbs_FACE 

23from OCC.Core.TopExp import TopExp_Explorer 

24from OCC.Core.TopLoc import TopLoc_Location 

25from OCC.Core.TopoDS import TopoDS_Iterator, TopoDS_Shape, topods_Face 

26from OCC.Core.gp import gp_Pnt, gp_XYZ 

27 

28from bim2sim.utilities.pyocc_tools import PyOCCTools 

29 

30Vertex = Vector = Tuple[float, float, float] 

31Edge = Tuple[Vertex, Vertex] 

32Triangulation = List[List[Vertex]] 

33Plane = Tuple[Vector, Vector, Vector] 

34T = TypeVar('T') 

35 

36 

37class _UnionFind(Generic[T]): 

38 """ 

39 Implementation of a union-find data structure with union-by-size and path 

40 compression. 

41 """ 

42 

43 def __init__(self): 

44 self._parents = dict() 

45 self._sizes = dict() 

46 

47 def union(self, element1: T, element2: T) -> T: 

48 key1 = self.find(element1) 

49 key2 = self.find(element2) 

50 if key1 == key2: 

51 return 

52 if self._sizes[key1] < self._sizes[key2]: 

53 key1, key2 = key2, key1 

54 self._parents[key2] = key1 

55 self._sizes[key1] += self._sizes[key2] 

56 

57 def find(self, element: T) -> T: 

58 if element not in self._parents: 

59 self._parents[element] = None 

60 self._sizes[element] = 1 

61 root = element 

62 while self._parents[root] is not None: 

63 root = self._parents[root] 

64 while self._parents[element] is not None: 

65 parent = self._parents[element] 

66 self._parents[element] = root 

67 element = parent 

68 return element 

69 

70 

71def _gp_pnt_to_coord_tuple(pnt: gp_Pnt) -> Vertex: 

72 """ 

73 Converts a gp_Pnt instance (3D point class of OCC) to a 3-tuple, because 

74 we need our vertex type to be comparable and hashable, which gp_Pnt is not. 

75 """ 

76 return pnt.X(), pnt.Y(), pnt.Z() 

77 

78 

79def _subshapes(shape): 

80 it = TopoDS_Iterator(shape) 

81 clist = [] 

82 while it.More(): 

83 clist.append(_subshapes(it.Value())) 

84 it.Next() 

85 return shape if len(clist) == 0 else shape, clist 

86 

87 

88def _get_triangulation(face: TopoDS_Shape) -> Triangulation: 

89 """ 

90 Calculates and extracts the triangulation of a TopoDS shape and returns it 

91 as a list of triangles. 

92 """ 

93 mesh = BRepMesh_IncrementalMesh(face, 0.01) 

94 mesh.Perform() 

95 ex = TopExp_Explorer(mesh.Shape(), TopAbs_FACE) 

96 bt = BRep_Tool() 

97 result = [] 

98 while ex.More(): 

99 L = TopLoc_Location() 

100 triangulation = bt.Triangulation(topods_Face(ex.Current()), L) 

101 if not triangulation: 

102 triangulation = bt.Triangulation(PyOCCTools.make_faces_from_pnts( 

103 PyOCCTools.get_points_of_face(topods_Face(ex.Current()))), L) 

104 if not triangulation: 

105 ex.Next() 

106 triangles = triangulation.Triangles() 

107 if hasattr(triangulation, 'Nodes'): 

108 vertices = triangulation.Nodes() 

109 else: 

110 vertices = [] 

111 for i in range(1, triangulation.NbNodes() + 1): 

112 vertices.append(triangulation.Node(i)) 

113 for i in range(1, triangulation.NbTriangles() + 1): 

114 idx1, idx2, idx3 = triangles.Value(i).Get() 

115 P1 = vertices[idx1-1].Transformed(L.Transformation()) 

116 P2 = vertices[idx2-1].Transformed(L.Transformation()) 

117 P3 = vertices[idx3-1].Transformed(L.Transformation()) 

118 result.append( 

119 [_gp_pnt_to_coord_tuple(P1), _gp_pnt_to_coord_tuple(P2), 

120 _gp_pnt_to_coord_tuple(P3)]) 

121 ex.Next() 

122 return result 

123 

124 

125def _normalize(edge: Edge) -> Edge: 

126 """ 

127 Edges are normalized so that (a,b) and (b,a) are both returned as (a,b). 

128 """ 

129 return (edge[0], edge[1]) if edge[0] < edge[1] else (edge[1], edge[0]) 

130 

131 

132def _iterate_edges(polygon: List[Vertex], directed: bool = False): 

133 """ 

134 Constructs an iterator for iterating over all edge of the given polygon. If 

135 directed is set to false, the returned edges are normalized. 

136 """ 

137 for i in range(len(polygon)): 

138 v1 = polygon[i] 

139 v2 = polygon[(i + 1) % len(polygon)] 

140 yield (v1, v2) if directed else _normalize((v1, v2)) 

141 

142 

143def _get_inside_outside_edges(triangulation: Triangulation) -> Tuple[ 

144 List[Edge], List[Edge]]: 

145 """ 

146 Partitions all edges of the triangulation into two lists, edges that lay 

147 "outside" and edges that lay "inside". Outside edges are part of the 

148 boundaries of either the main polygon or one of its holes, while inside 

149 edges were added by the triangulation step. 

150 

151 Outside edges are returned in their "correct" direction, inside edges are 

152 not. 

153 """ 

154 edge_count = defaultdict(int) 

155 orientation = dict() 

156 inside, outside = [], [] 

157 

158 # We count how many times a particular edge is occuring in the 

159 # triangulation, because every inside edge is part of two triangles, 

160 # while edges laying on the polygon boundaries are only part of a 

161 # single triangulation. 

162 for triangle in triangulation: 

163 for edge in _iterate_edges(triangle, True): 

164 edge_count[_normalize(edge)] += 1 

165 orientation[_normalize(edge)] = edge 

166 for edge, count in edge_count.items(): 

167 if count == 1: 

168 outside.append(orientation[edge]) 

169 else: 

170 inside.append(edge) 

171 return inside, outside 

172 

173 

174def _get_jump_map(cut_edges: List[Edge], out_edges: List[Edge], plane: Plane) \ 

175 -> Mapping[Vertex, List[Vertex]]: 

176 """ 

177 Constructs a jump map based on a list on cut edges, so that for every edge 

178 (a,b), map[a] contains b and map[b] contains a. 

179  

180 Things get complicated when we have more than one entry for a vertex, which 

181 is the case when more than one edge in the spanning tree are incident to a 

182 vertex. Then we have to order the entries in clockwise order so that the 

183 polygon reconstruction builds a valid polygon that does not self-intersect. 

184 This is ALSO complicated by the fact that we are in 3 dimensions (which we 

185 could ignore until now), but at least we can assume that all input points 

186 lie in a common plane. Ordering the entries is done in _order_points_cw. 

187 """ 

188 out_dest = dict() 

189 for edge in out_edges: 

190 out_dest[edge[0]] = edge[1] 

191 

192 jump_map = defaultdict(list) 

193 for edge in cut_edges: 

194 jump_map[edge[0]].append(edge[1]) 

195 jump_map[edge[1]].append(edge[0]) 

196 

197 for key, values in jump_map.items(): 

198 if len(values) <= 1: 

199 continue 

200 jump_map[key] = _order_points_cw(plane, key, out_dest[key], values) 

201 return jump_map 

202 

203 

204def _order_points_cw(plane: Plane, mid: Vector, control: Vector, 

205 vertices: List[Vertex]) -> List[Vertex]: 

206 """ 

207 Based on: 

208 https://stackoverflow.com/questions/47949485/sorting-a-list-of-3d-points-in-clockwise-order 

209 

210 To sort points laying in a plane in 3D space in clockwise order around a 

211 given origin m, we have to 

212 

213 1. find the normal vector n of this plane 

214 2. find vectors p, q in our plane such that n, p, q are perpendicular 

215 to each other and form a right-handed system. 

216 3. Calculate for every vertex v the triple products u = n * ((v-m) x p) 

217 and t = n * ((v-m) x q) to obtain a sort key of atan2(u, t). 

218 

219 We take n, p and q as arguments because we just have to calculate them once. 

220 

221 We also take a control vector, which is also sorted alongside the other 

222 vertices. The result is then shifted so that the control vector is the first 

223 element in the result list. The control vector is not included in the 

224 returned list. 

225 """ 

226 n, p, q = plane 

227 

228 def sort_key(v: Vertex) -> float: 

229 t = numpy.dot(n, numpy.cross(numpy.subtract(v, mid), p)) 

230 u = numpy.dot(n, numpy.cross(numpy.subtract(v, mid), q)) 

231 return math.atan2(u, t) 

232 

233 s = sorted([control] + vertices, key=sort_key) 

234 roll = s.index(control) 

235 rolled = s[roll:] + s[:roll] 

236 return rolled[1:] 

237 

238 

239def _calculate_plane_vectors(vertices: List[Vertex]) -> Plane: 

240 """ 

241 Calculates n, p and q describing the plane the input values lie in. More 

242 info see _order_points_cw. 

243 """ 

244 a, b, c = vertices 

245 d = numpy.cross(numpy.subtract(b, a), numpy.subtract(c, a)) 

246 n = numpy.divide(d, numpy.linalg.norm(d)) 

247 

248 # We try two different unit vectors for the case that n and the first chosen unit vector are 

249 # parallel. 

250 p = (0, 0, 0) 

251 for uvec in [(1, 0, 0), (0, 1, 0)]: 

252 np = numpy.cross(uvec, n) 

253 if numpy.linalg.norm(p) < numpy.linalg.norm(np): 

254 p = np 

255 q = numpy.cross(n, p) 

256 

257 return tuple(n), tuple(p), tuple(q) 

258 

259 

260def _reconstruct_polygons(edges: List[Edge]) -> List[List[Vertex]]: 

261 """ 

262 Takes a list of edges in any order and reconstructs the correctly ordered 

263 vertices of the polygons formed by the given edges. It is assumed that the 

264 edges in the input are reconstructable to some list of polygons. 

265 """ 

266 result = [] 

267 chain = dict() 

268 for edge in edges: 

269 chain[edge[0]] = edge[1] 

270 while len(chain) > 0: 

271 start = next( 

272 iter(chain)) # use any key in the chain, we don't care which. 

273 polygon = [] 

274 key = start 

275 first = True 

276 while key != start or first: 

277 first = False 

278 polygon.append(key) 

279 next_key = chain[key] 

280 del chain[key] 

281 key = next_key 

282 result.append(polygon) 

283 return result 

284 

285 

286def _index_polygon_vertices(polygons: List[List[Vertex]]) -> Mapping[ 

287 Vertex, Tuple[int, int]]: 

288 """ 

289 Build a index map where map[v] = (i,j) <=> polygons[i][j] = v. 

290 """ 

291 index = dict() 

292 for pIdx, polygon in enumerate(polygons): 

293 for vIdx, vertex in enumerate(polygon): 

294 index[vertex] = (pIdx, vIdx) 

295 return index 

296 

297 

298def _reconstruct_cut_polygon(out_edges: List[Edge], cut_edges: List[Edge], 

299 plane: Plane) -> List[Vertex]: 

300 """ 

301 Takes a list of outside edges and a list of cut edges and reconstructs the 

302 single polygon they form. 

303 """ 

304 polygons = _reconstruct_polygons(out_edges) 

305 jump = dict(_get_jump_map(cut_edges, out_edges, plane)) 

306 poly_index = _index_polygon_vertices(polygons) 

307 

308 # Finds the index of the first vertex we can start on. We only start on 

309 # vertices that don't have any jumps to make the exit condition of our main 

310 # loop simpler. It is guaranteed that such a vertex exists (edges in a 

311 # spanning tree over all polygons < 3 * vertices in all polygons). 

312 def find_start_index(): 

313 for i1, polygon in enumerate(polygons): 

314 for i2, vertex in enumerate(polygon): 

315 if vertex not in jump: 

316 return i1, i2 

317 

318 cut_polygon = [] 

319 start_index = find_start_index() 

320 idx = start_index 

321 last_jump_source: Optional[Vertex] = None 

322 

323 while True: 

324 current = polygons[idx[0]][idx[1]] 

325 cut_polygon.append(current) 

326 assert len(cut_polygon) <= len( 

327 out_edges) * 2, "Infinite loop detected. Arguments are not right." 

328 

329 # We find out if we have to jump to another polygon. If yes, this 

330 # variable will be set to the index of the jump list of our current 

331 # vertex to where we have to jump. 

332 jump_list_index = None 

333 if current in jump: 

334 # There are jumps for the current vertex. Check if we already jumped 

335 # the last time. 

336 if last_jump_source is None: 

337 # ... no, so just jump to the last (= first in ccw order) entry 

338 # in the jump list. 

339 jump_list_index = -1 

340 else: 

341 # ... yes, so we have to jump to the first entry BEFORE the one 

342 # (= first after in ccw order) 

343 # we just came from. If we already came from the first entry, we 

344 # instead start to traverse 

345 # the current polygon. 

346 last_jump_index = jump[current].index(last_jump_source) 

347 if last_jump_index > 0: 

348 jump_list_index = last_jump_index - 1 

349 

350 if jump_list_index is not None: 

351 jump_vertex = jump[current][jump_list_index] 

352 idx = poly_index[jump_vertex] 

353 last_jump_source = current 

354 else: 

355 last_jump_source = None 

356 idx = (idx[0], (idx[1] + 1) % len(polygons[idx[0]])) 

357 

358 # Because we started on a vertex that has no jumps, the start vertex can 

359 # only be visited once during polygon reconstruction. Therefore, if we 

360 # arrive at our start index a second time we are done. 

361 if idx == start_index: 

362 break 

363 

364 return cut_polygon 

365 

366 

367def remove_inner_loops(shape: TopoDS_Shape) -> TopoDS_Shape: 

368 # Build all necessary data structures. 

369 triangulation = _get_triangulation(shape) 

370 in_edges, out_edges = _get_inside_outside_edges(triangulation) 

371 partition = _UnionFind() 

372 

373 plane = _calculate_plane_vectors(triangulation[0]) 

374 

375 # Build initial partition state. After that, every loop (either the main 

376 # polygon or a hole) 

377 # is in its own disjoint set. 

378 for edge in out_edges: 

379 partition.union(edge[0], edge[1]) 

380 

381 # We now find a spanning tree by applying Kruskal's algorithm to the 

382 # triangulation graph. Note that in an unweighted graph, every spanning 

383 # tree is a minimal spanning tree, so it doesn't actually matter which 

384 # spanning tree we are calculating here. Edges that are part of the 

385 # spanning tree are pushed into cut_edges. 

386 cut_edges = [] 

387 for edge in in_edges: 

388 if partition.find(edge[0]) != partition.find(edge[1]): 

389 cut_edges.append(edge) 

390 partition.union(edge[0], edge[1]) 

391 

392 cut_polygon = _reconstruct_cut_polygon(out_edges, cut_edges, plane) 

393 new_shape = PyOCCTools.make_faces_from_pnts(cut_polygon) 

394 

395 # Copy over shape location 

396 # shape_loc = TopoDS_Iterator(shape).Value().Location() 

397 # new_shape.Move(shape_loc) 

398 

399 return new_shape 

400 

401 

402def _cross(a: Vertex, b: Vertex) -> Vertex: 

403 return a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - \ 

404 a[1] * b[0] 

405 

406 

407def _dot(a: Vertex, b: Vertex) -> float: 

408 return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] 

409 

410 

411def _minus(a: Vertex, b: Vertex) -> Vertex: 

412 return a[0] - b[0], a[1] - b[1], a[2] - b[2] 

413 

414 

415def _is_convex_angle(p1: Vertex, p2: Vertex, p3: Vertex, 

416 normal: Vertex) -> bool: 

417 cross = _cross(_minus(p2, p1), _minus(p3, p1)) 

418 return _dot(cross, normal) >= -1e-6 

419 

420 

421def fuse_pieces(pieces: List[List[Vertex]], 

422 shapes_to_consider: List[TopoDS_Shape] = []) -> List[ 

423 List[Vertex]]: 

424 normal, _, _ = _calculate_plane_vectors(pieces[0]) 

425 consider_polygons = False 

426 

427 if len(shapes_to_consider) > 0: 

428 consider_polygons = True 

429 edges = [] 

430 for shape in shapes_to_consider: 

431 list_pnts = PyOCCTools.get_points_of_face(shape) 

432 for i, p in enumerate(list_pnts[:-1]): 

433 edges.append(BRepBuilderAPI_MakeEdge(list_pnts[i], 

434 list_pnts[i + 1]).Shape()) 

435 edges.append( 

436 BRepBuilderAPI_MakeEdge(list_pnts[-1], list_pnts[0]).Shape()) 

437 

438 i1 = 0 

439 while i1 < len(pieces) - 1: 

440 piece_a = pieces[i1] 

441 i1 += 1 

442 for piece_a_idx in range(0, len(piece_a)): 

443 a1 = piece_a[piece_a_idx] 

444 a2 = piece_a[(piece_a_idx + 1) % len(piece_a)] 

445 

446 is_inner_edge = False 

447 piece_b = None 

448 piece_b_idx = None 

449 for i2 in range(i1, len(pieces)): 

450 piece_b = pieces[i2] 

451 for triangle_b_check_idx in range(0, len(piece_b)): 

452 if a2 != piece_b[triangle_b_check_idx]: 

453 continue 

454 if a1 != piece_b[(triangle_b_check_idx + 1) % len(piece_b)]: 

455 continue 

456 piece_b_idx = triangle_b_check_idx 

457 is_inner_edge = True 

458 break 

459 if is_inner_edge: 

460 break 

461 if not is_inner_edge: 

462 continue 

463 

464 # piece_a and piece_b are two triangles with a common edge (piece_a 

465 # and piece_b) 

466 # common edge is spanned between a1 and a2 

467 p1 = piece_a[(piece_a_idx - 1) % len(piece_a)] 

468 p2 = a1 

469 p3 = piece_b[(piece_b_idx + 2) % len(piece_b)] 

470 

471 if not consider_polygons: 

472 # if no polygons need to be considered, shapes are fused 

473 # unless resulting shape is non-convex 

474 if not _is_convex_angle(p1, p2, p3, normal): 

475 continue 

476 else: 

477 # if an edge from triangulation cuts the edge of a polygon 

478 # which needs to be considered (e.g. from an opening boundary 

479 # within a boundary of a wall), then those shapes have to be 

480 # fused regardless of the resulting angle 

481 # this may lead to non-convex shapes in some cases 

482 a1_edge = BRepBuilderAPI_MakeEdge(gp_Pnt(*a1), 

483 gp_Pnt(*a2)).Shape() 

484 continue_flag = True 

485 for edge in edges: 

486 if BRepExtrema_DistShapeShape( 

487 edge, a1_edge, Extrema_ExtFlag_MIN).Value() < 1e-3: 

488 continue_flag = False 

489 else: 

490 pass 

491 if continue_flag: 

492 continue 

493 

494 # procedure is repeated for common edge of neighboring shape 

495 p1 = piece_b[(piece_b_idx - 1) % len(piece_b)] 

496 p2 = a2 

497 p3 = piece_a[(piece_a_idx + 2) % len(piece_a)] 

498 if not consider_polygons: 

499 if not _is_convex_angle(p1, p2, p3, normal): 

500 continue 

501 else: 

502 a2_edge = BRepBuilderAPI_MakeEdge(gp_Pnt(*a2), 

503 gp_Pnt(*a1)).Shape() 

504 continue_flag = True 

505 for edge in edges: 

506 if BRepExtrema_DistShapeShape( 

507 edge, a2_edge, Extrema_ExtFlag_MIN).Value() < 1e-3: 

508 continue_flag = False 

509 else: 

510 pass 

511 if continue_flag: 

512 continue 

513 # fuse triangles (if angle is convex or opening-polygon is cut by 

514 # this edge 

515 fused_piece = [] 

516 i = (piece_a_idx + 1) % len(piece_a) 

517 while i != piece_a_idx: 

518 fused_piece.append(piece_a[i]) 

519 i = (i + 1) % len(piece_a) 

520 i = (piece_b_idx + 1) % len(piece_b) 

521 while i != piece_b_idx: 

522 fused_piece.append(piece_b[i]) 

523 i = (i + 1) % len(piece_b) 

524 

525 i1 -= 1 

526 pieces.remove(piece_a) 

527 pieces.remove(piece_b) 

528 pieces.insert(i1, fused_piece) 

529 piece_a = fused_piece 

530 break 

531 return pieces 

532 

533 

534def convex_decomposition_base(shape: TopoDS_Shape, 

535 opening_shapes: List[TopoDS_Shape] = []) -> List[ 

536 List[Vertex]]: 

537 """Convex decomposition base: removes common edges of triangles unless a 

538 non-convex shape is created. 

539 In case of openings: In a first round, remove all cutting triangle edges 

540 with the opening polygons regardless of non-convex shapes. 

541 Then, check for resulting angles. This may lead to non-convex shapes, 

542 but should work in most cases. 

543 """ 

544 pieces = _get_triangulation(shape) 

545 if len(opening_shapes) > 0: 

546 pieces = fuse_pieces(pieces, opening_shapes) 

547 pieces = fuse_pieces(pieces) 

548 

549 return pieces 

550 

551 

552def convex_decomposition(shape: TopoDS_Shape, 

553 opening_shapes: List[TopoDS_Shape] = []) -> List[ 

554 TopoDS_Shape]: 

555 pieces = convex_decomposition_base(shape, opening_shapes) 

556 pieces_area = 0 

557 new_area = 0 

558 new_pieces = [] 

559 for p in pieces: 

560 pieces_area += PyOCCTools.get_shape_area( 

561 PyOCCTools.make_faces_from_pnts(p)) 

562 pnt_list_new = PyOCCTools.remove_coincident_vertices( 

563 [gp_XYZ(pnt[0], pnt[1], pnt[2]) for pnt in p]) 

564 pnt_list_new = PyOCCTools.remove_collinear_vertices2(pnt_list_new) 

565 if pnt_list_new != p and len(pnt_list_new) > 3: 

566 pnt_list_new = [n.Coord() for n in pnt_list_new] 

567 p = pnt_list_new 

568 new_pieces.append(p) 

569 new_area += PyOCCTools.get_shape_area( 

570 PyOCCTools.make_faces_from_pnts(p)) 

571 if abs(pieces_area - new_area) > 1e-3: 

572 new_pieces = pieces 

573 new_shapes = list( 

574 map(lambda p: PyOCCTools.make_faces_from_pnts(p), new_pieces)) 

575 oriented_shapes = [] 

576 org_normal = PyOCCTools.simple_face_normal(shape) 

577 for new_shape in new_shapes: 

578 new_normal = PyOCCTools.simple_face_normal(new_shape) 

579 if all([abs(i) < 1e-3 for i in ((new_normal - org_normal).Coord())]): 

580 oriented_shapes.append(new_shape) 

581 else: 

582 new_shape = PyOCCTools.flip_orientation_of_face(new_shape) 

583 new_normal = PyOCCTools.simple_face_normal(new_shape) 

584 if all([abs(i) < 1e-3 for i in 

585 ((new_normal - org_normal).Coord())]): 

586 oriented_shapes.append(new_shape) 

587 else: 

588 logger = logging.getLogger(__name__) 

589 logger.error( 

590 "Convex decomposition produces a gap in new space boundary") 

591 # check if decomposed shape has same area as original shape 

592 oriented_area = 0 

593 org_area = PyOCCTools.get_shape_area(shape) 

594 for face in oriented_shapes: 

595 oriented_area += PyOCCTools.get_shape_area(face) 

596 cut_count = 0 

597 while abs(org_area - oriented_area) > 5e-3: 

598 cut_count += 1 

599 cut_shape = shape 

600 for bound in oriented_shapes: 

601 cut_shape = BRepAlgoAPI_Cut(cut_shape, bound).Shape() 

602 list_cut_shapes = PyOCCTools.get_faces_from_shape(cut_shape) 

603 add_cut_shapes = [] 

604 for cs in list_cut_shapes: 

605 new_normal = PyOCCTools.simple_face_normal(cs) 

606 if not all([abs(i) < 1e-3 for i in 

607 ((new_normal - org_normal).Coord())]): 

608 cs = PyOCCTools.flip_orientation_of_face(cs) 

609 cut_area = PyOCCTools.get_shape_area(cs) 

610 if cut_area < 5e-4: 

611 continue 

612 cs = PyOCCTools.remove_coincident_and_collinear_points_from_face(cs) 

613 oriented_area += cut_area 

614 add_cut_shapes.append(cs) 

615 if cut_count > 3: 

616 logger = logging.getLogger(__name__) 

617 logger.error( 

618 "Convex decomposition produces a gap in new space boundary") 

619 break 

620 else: 

621 oriented_shapes.extend(add_cut_shapes) 

622 return oriented_shapes 

623 

624 

625def is_convex_slow(shape: TopoDS_Shape) -> bool: 

626 """ 

627 Computational expensive check if a TopoDS_Shape is convex. 

628 Args: 

629 shape: TopoDS_Shape 

630 

631 Returns: 

632 bool, True if shape is convex. 

633 """ 

634 return len(convex_decomposition_base(shape)) == 1 

635 

636 

637def is_convex_no_holes(shape: TopoDS_Shape) -> bool: 

638 """check if TopoDS_Shape is convex. Returns False if shape is non-convex""" 

639 gp_pnts = PyOCCTools.get_points_of_face(shape) 

640 pnts = list(map(lambda p: _gp_pnt_to_coord_tuple(p), gp_pnts)) 

641 z = 0 

642 for i in range(0, len(pnts)): 

643 p0 = pnts[i] 

644 p1 = pnts[(i + 1) % len(pnts)] 

645 p2 = pnts[(i + 2) % len(pnts)] 

646 cross = _cross(_minus(p1, p0), _minus(p2, p1))[2] 

647 if z != 0 and abs(cross) >= 1e-6 and numpy.sign(cross) != numpy.sign(z): 

648 return False 

649 else: 

650 z = cross 

651 return True 

652 

653 

654def is_polygon_convex_no_holes(pnts: List[Tuple[float, float, float]]) -> bool: 

655 """check if polygon made from tuples of floats is convex. 

656 Returns False if shape is non-convex""" 

657 z = 0 

658 for i in range(0, len(pnts)): 

659 p0 = pnts[i] 

660 p1 = pnts[(i + 1) % len(pnts)] 

661 p2 = pnts[(i + 2) % len(pnts)] 

662 cross = _cross(_minus(p1, p0), _minus(p2, p1))[2] 

663 if z != 0 and abs(cross) >= 1e-6 and numpy.sign(cross) != numpy.sign(z): 

664 return False 

665 else: 

666 z = cross 

667 return True