Coverage for bim2sim / plugins / PluginEnergyPlus / bim2sim_energyplus / utils / view_space_boundaries.py: 0%

487 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-18 09:34 +0000

1# DISCLAIMER: Viewer has been generated by GPT5. Not all functions may work 

2# as expected, but this viewer helps to load and display space boundaries 

3# from IFC (incl. a setup from scratch). 

4 

5 

6import sys 

7from typing import List, Tuple, Optional 

8 

9import ifcopenshell 

10import ifcopenshell.geom 

11 

12from OCC.Core.TopoDS import TopoDS_Shape 

13from OCC.Core.gp import gp_Trsf 

14from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform 

15 

16# Optional: viewer for quick inspection (uses pythonocc) 

17from OCC.Display.SimpleGui import init_display 

18from OCC.Core.ShapeFix import ShapeFix_Shape, ShapeFix_Face, ShapeFix_Wire 

19from OCC.Core.ShapeAnalysis import ShapeAnalysis_FreeBounds 

20from OCC.Core.TopExp import TopExp_Explorer 

21from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_WIRE, TopAbs_VERTEX 

22from OCC.Core.TopoDS import topods, TopoDS_Compound 

23from OCC.Core.BRep import BRep_Builder 

24from OCC.Core.BRep import BRep_Tool 

25from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace 

26from OCC.Core.gp import gp_Vec 

27from OCC.Core.Geom import Geom_Plane 

28from OCC.Core.ShapeFix import ShapeFix_ShapeTolerance 

29from OCC.Core.BRepCheck import BRepCheck_Analyzer 

30from OCC.Core.TopAbs import TopAbs_EDGE 

31 

32from bim2sim.utilities.pyocc_tools import PyOCCTools 

33 

34 

35def heal_boundary_shape(shape: TopoDS_Shape) -> TopoDS_Shape: 

36 # Global shape fix and tolerance normalization 

37 try: 

38 sfix = ShapeFix_Shape(shape) 

39 sfix.Perform() 

40 shape = sfix.Shape() 

41 except Exception: 

42 pass 

43 try: 

44 stol = ShapeFix_ShapeTolerance() 

45 stol.SetMinTolerance(shape, 1e-7) 

46 stol.SetMaxTolerance(shape, 1e-3) 

47 stol.SetTolerance(shape, 1e-5) 

48 except Exception: 

49 pass 

50 try: 

51 BRepLib.BuildCurves3d(shape) 

52 BRepLib.SameParameter(shape, 1e-7) 

53 except Exception: 

54 pass 

55 

56 faces = [] 

57 

58 # Pass 1: fix any existing faces 

59 try: 

60 exp_face = TopExp_Explorer(shape, TopAbs_FACE) 

61 while exp_face.More(): 

62 face = topods.Face(exp_face.Current()) 

63 try: 

64 sff = ShapeFix_Face(face) 

65 sff.SetPrecision(1e-7) 

66 sff.SetMaxTolerance(1e-3) 

67 sff.FixOrientation() 

68 sff.FixWires() 

69 try: 

70 wtool = sff.FixWireTool() 

71 wtool.SetPrecision(1e-7) 

72 wtool.SetMinTolerance(1e-7) 

73 wtool.SetMaxTolerance(1e-3) 

74 except Exception: 

75 pass 

76 sff.Perform() 

77 face = sff.Face() 

78 except Exception: 

79 pass 

80 if not face.IsNull(): 

81 faces.append(face) 

82 exp_face.Next() 

83 except Exception: 

84 pass 

85 

86 # Helper: infer a plane from 3 non-collinear points 

87 def _infer_plane_from_points(pts): 

88 for i in range(len(pts)): 

89 for j in range(i + 1, len(pts)): 

90 for k in range(j + 1, len(pts)): 

91 v1 = gp_Vec(pts[i], pts[j]) 

92 v2 = gp_Vec(pts[i], pts[k]) 

93 if v1.Crossed(v2).Magnitude() > 1e-12: 

94 return Geom_Plane(pts[i], pts[j], pts[k]).Pln() 

95 return None 

96 

97 # Helper: ordered vertices from a wire (after wire fix) 

98 def _ordered_points_from_wire(wire, tol=1e-7): 

99 pts = [] 

100 try: 

101 # Explore edges in wire order 

102 wx = BRepTools_WireExplorer(wire) 

103 prev_pt = None 

104 first = True 

105 while wx.More(): 

106 e = wx.Current() 

107 # Get end vertices 

108 v1 = TopExp.FirstVertex(e) 

109 v2 = TopExp.LastVertex(e) 

110 p1 = BRep_Tool.Pnt(v1) 

111 p2 = BRep_Tool.Pnt(v2) 

112 

113 def _dist(a, b): 

114 return gp_Vec(a, b).Magnitude() 

115 

116 if first: 

117 pts.append(p1) 

118 pts.append(p2) 

119 prev_pt = pts[-1] 

120 first = False 

121 else: 

122 # Append the vertex that continues the chain 

123 if _dist(prev_pt, p1) <= tol: 

124 pts.append(p2) 

125 prev_pt = p2 

126 elif _dist(prev_pt, p2) <= tol: 

127 pts.append(p1) 

128 prev_pt = p1 

129 else: 

130 # If neither matches (edge orientation issue), append both to keep continuity 

131 # This is rare after FixReorder; still handle gracefully 

132 pts.append(p1) 

133 pts.append(p2) 

134 prev_pt = p2 

135 wx.Next() 

136 

137 # Remove consecutive duplicates 

138 cleaned = [] 

139 for p in pts: 

140 if not cleaned or gp_Vec(cleaned[-1], p).Magnitude() > tol: 

141 cleaned.append(p) 

142 # Ensure closed 

143 if cleaned and gp_Vec(cleaned[0], cleaned[-1]).Magnitude() > tol: 

144 cleaned.append(cleaned[0]) 

145 return cleaned 

146 except Exception: 

147 return pts 

148 

149 # 2D geometry helpers on a plane 

150 def _pts3d_to_uv(plane, pts3d): 

151 uvs = [] 

152 for p in pts3d: 

153 try: 

154 u, v = ElSLib.Parameters(plane, p) 

155 except Exception: 

156 # Fallback if Parameters fails (rare): project using plane axes 

157 ax = plane.Position() 

158 vec = gp_Vec(ax.Location(), p) 

159 ux = gp_Vec(ax.XDirection().XYZ()) 

160 uy = gp_Vec(ax.YDirection().XYZ()) 

161 u = vec.Dot(ux) 

162 v = vec.Dot(uy) 

163 uvs.append((float(u), float(v))) 

164 return uvs 

165 

166 def _uv_to_pts3d(plane, uvs): 

167 pts = [] 

168 for u, v in uvs: 

169 pts.append(ElSLib.Value(u, v, plane)) 

170 return pts 

171 

172 # Segment intersection in 2D 

173 def _ccw(a, b, c): 

174 return (c[1] - a[1]) * (b[0] - a[0]) > (b[1] - a[1]) * (c[0] - a[0]) 

175 

176 def _seg_intersect(a, b, c, d): 

177 # Proper intersection (exclude touching endpoints) 

178 return _ccw(a, c, d) != _ccw(b, c, d) and _ccw(a, b, c) != _ccw(a, b, d) 

179 

180 # 2-opt reordering to remove crossings 

181 def _two_opt_simple_polygon(uvs, max_iters=50): 

182 if len(uvs) < 4: 

183 return uvs 

184 # Work on open list (last equals first), ignore duplicate last 

185 closed = gp_Vec(gp_Pnt(uvs[0][0], uvs[0][1], 0), gp_Pnt(uvs[-1][0], uvs[-1][1], 0)).Magnitude() < 1e-12 

186 pts = uvs[:-1] if closed else list(uvs) 

187 n = len(pts) 

188 

189 def _area(poly): 

190 a = 0.0 

191 for i in range(n): 

192 x1, y1 = poly[i] 

193 x2, y2 = poly[(i + 1) % n] 

194 a += x1 * y2 - x2 * y1 

195 return a * 0.5 

196 

197 # Ensure CCW orientation (optional) 

198 if _area(pts) < 0: 

199 pts.reverse() 

200 

201 changed = True 

202 it = 0 

203 while changed and it < max_iters: 

204 changed = False 

205 it += 1 

206 for i in range(n - 1): 

207 a1 = pts[i] 

208 a2 = pts[(i + 1) % n] 

209 for j in range(i + 2, n - (0 if i > 0 else 1)): 

210 b1 = pts[j] 

211 b2 = pts[(j + 1) % n] 

212 if _seg_intersect(a1, a2, b1, b2): 

213 # Reverse the sub-chain to remove intersection 

214 pts[i + 1:j + 1] = reversed(pts[i + 1:j + 1]) 

215 changed = True 

216 break 

217 if changed: 

218 break 

219 

220 # Close loop 

221 pts.append(pts[0]) 

222 return pts 

223 

224 # Build a face from a wire, with polygon fallback 

225 def _build_face_from_wire(wire: TopoDS_Shape): 

226 # First: aggressive wire fix 

227 try: 

228 try: 

229 wfix = ShapeFix_Wire(topods.Wire(wire)) 

230 except Exception: 

231 wfix = ShapeFix_Wire(wire) 

232 wfix.SetPrecision(1e-7) 

233 wfix.SetMinTolerance(1e-7) 

234 wfix.SetMaxTolerance(1e-3) 

235 try: 

236 wfix.SetClosed(True) 

237 except Exception: 

238 pass 

239 wfix.FixReorder() 

240 wfix.FixConnected() 

241 wfix.FixSmall(Precision.Confusion()) 

242 wfix.FixDegenerated() 

243 wfix.FixSelfIntersection() 

244 wfix.FixLacking() 

245 wfix.Perform() 

246 wire = wfix.Wire() 

247 except Exception: 

248 pass 

249 

250 # Try direct face creation 

251 try: 

252 mf = BRepBuilderAPI_MakeFace(wire, True) 

253 f = mf.Face() 

254 if not f.IsNull() and BRepCheck_Analyzer(f).IsValid(): 

255 sff = ShapeFix_Face(f) 

256 sff.SetPrecision(1e-7) 

257 sff.SetMaxTolerance(1e-3) 

258 sff.Perform() 

259 return sff.Face() 

260 except Exception: 

261 pass 

262 

263 # Polygon fallback: reorder vertices to remove crossings and rebuild planar wire 

264 # Gather ordered points and infer plane 

265 try: 

266 pts3d = _ordered_points_from_wire(wire, tol=1e-6) 

267 if len(pts3d) >= 3: 

268 plane = _infer_plane_from_points(pts3d) 

269 if plane: 

270 uvs = _pts3d_to_uv(plane, pts3d) 

271 uvs = _two_opt_simple_polygon(uvs, max_iters=80) 

272 pts3d_clean = _uv_to_pts3d(plane, uvs) 

273 

274 # Rebuild wire from straight segments on the plane 

275 try: 

276 mw = BRepBuilderAPI_MakeWire() 

277 for i in range(len(pts3d_clean) - 1): 

278 e = BRepBuilderAPI_MakeEdge(pts3d_clean[i], pts3d_clean[i + 1]).Edge() 

279 mw.Add(e) 

280 if mw.IsDone(): 

281 w2 = mw.Wire() 

282 # Heal new wire and create face 

283 try: 

284 wfix2 = ShapeFix_Wire(w2) 

285 wfix2.SetPrecision(1e-7) 

286 wfix2.SetMinTolerance(1e-7) 

287 wfix2.SetMaxTolerance(1e-3) 

288 try: 

289 wfix2.SetClosed(True) 

290 except Exception: 

291 pass 

292 wfix2.FixReorder() 

293 wfix2.FixConnected() 

294 wfix2.FixSmall(Precision.Confusion()) 

295 wfix2.FixDegenerated() 

296 wfix2.Perform() 

297 w2 = wfix2.Wire() 

298 except Exception: 

299 pass 

300 

301 # Try direct face; if still problematic, force plane context 

302 try: 

303 mf2 = BRepBuilderAPI_MakeFace(w2, True) 

304 f2 = mf2.Face() 

305 if not f2.IsNull() and BRepCheck_Analyzer(f2).IsValid(): 

306 sff2 = ShapeFix_Face(f2) 

307 sff2.SetPrecision(1e-7) 

308 sff2.SetMaxTolerance(1e-3) 

309 sff2.Perform() 

310 return sff2.Face() 

311 except Exception: 

312 pass 

313 

314 try: 

315 mf3 = BRepBuilderAPI_MakeFace(plane, w2, True) 

316 f3 = mf3.Face() 

317 if not f3.IsNull(): 

318 sff3 = ShapeFix_Face(f3) 

319 sff3.SetPrecision(1e-7) 

320 sff3.SetMaxTolerance(1e-3) 

321 sff3.Perform() 

322 return sff3.Face() 

323 except Exception: 

324 pass 

325 except Exception: 

326 pass 

327 except Exception: 

328 pass 

329 

330 # Plane fallback from original wire if all else fails 

331 try: 

332 pts = [] 

333 expV = TopExp_Explorer(wire, TopAbs_VERTEX) 

334 while expV.More(): 

335 v = topods.Vertex(expV.Current()) 

336 pts.append(BRep_Tool.Pnt(v)) 

337 expV.Next() 

338 plane = _infer_plane_from_points(pts) 

339 if plane: 

340 mf = BRepBuilderAPI_MakeFace(plane, topods.Wire(wire), True) 

341 f = mf.Face() 

342 if not f.IsNull(): 

343 sff = ShapeFix_Face(f) 

344 sff.SetPrecision(1e-7) 

345 sff.SetMaxTolerance(1e-3) 

346 sff.Perform() 

347 return sff.Face() 

348 except Exception: 

349 pass 

350 

351 return None 

352 

353 # Pass 2: if no faces, build from closed wires with polygon fallback 

354 if not faces: 

355 try: 

356 fb = ShapeAnalysis_FreeBounds(shape, 1e-6, False, False) 

357 closed = fb.GetClosedWires() 

358 exp_wire = TopExp_Explorer(closed, TopAbs_WIRE) 

359 while exp_wire.More(): 

360 wire = topods.Wire(exp_wire.Current()) 

361 f = _build_face_from_wire(wire) 

362 if f and not f.IsNull(): 

363 faces.append(f) 

364 exp_wire.Next() 

365 except Exception: 

366 pass 

367 

368 # Assemble faces into a compound 

369 if faces: 

370 builder = BRep_Builder() 

371 comp = TopoDS_Compound() 

372 builder.MakeCompound(comp) 

373 for f in faces: 

374 try: 

375 builder.Add(comp, f) 

376 except Exception: 

377 continue 

378 try: 

379 BRepLib.SameParameter(comp, 1e-7) 

380 except Exception: 

381 pass 

382 return comp 

383 

384 # Fallback 

385 return shape 

386 

387def display_occ_shapes(ifc, shapes): 

388 """Display TopoDS_Shapes of space boundaries""" 

389 display, start_display, add_menu, add_function_to_menu = init_display() 

390 all_relbuild_elems = [] 

391 for guid, shape in shapes: 

392 bound = ifc.by_guid(guid) 

393 relbuildelem = bound.RelatedBuildingElement 

394 # 'blue', 'red', 'magenta', 'yellow', 'green', 'white', 'cyan' 

395 if relbuildelem: 

396 if relbuildelem.is_a('IfcWall'): 

397 color = 'blue' 

398 elif relbuildelem.is_a('IfcSlab'): 

399 color = 'red' 

400 elif relbuildelem.is_a('IfcBuildingElementProxy'): 

401 color = 'yellow' 

402 elif relbuildelem.is_a('IfcDoor'): 

403 color = 'green' 

404 elif relbuildelem.is_a('IfcWindow'): 

405 color = 'green' 

406 elif relbuildelem.is_a('IfcCurtainWall'): 

407 color = 'cyan' 

408 elif relbuildelem.is_a('IfcCovering'): 

409 color = 'orange' 

410 elif relbuildelem.is_a('IfcColumn'): 

411 color = 'black' 

412 elif relbuildelem.is_a('IfcBuildingElementProxy'): 

413 color = 'red' 

414 else: 

415 color = 'darkblue' 

416 print(relbuildelem.is_a()) 

417 all_relbuild_elems.append(relbuildelem.is_a()) 

418 else: 

419 color = 'white' 

420 try: 

421 display.DisplayShape(shape, color=color) 

422 except Exception: 

423 continue 

424 print(set(all_relbuild_elems)) 

425 display.FitAll() 

426 start_display() 

427 

428 

429# --- Placement utilities (IfcLocalPlacement -> gp_Trsf) --- 

430 

431def _to_float_list(seq): 

432 return [float(x) for x in seq] 

433 

434 

435def _norm3(v): 

436 x, y, z = v 

437 n = (x*x + y*y + z*z) ** 0.5 

438 return (x / n, y / n, z / n) if n else (0.0, 0.0, 1.0) 

439 

440 

441def _cross(a, b): 

442 ax, ay, az = a 

443 bx, by, bz = b 

444 return (ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx) 

445 

446 

447def _axis2placement_to_basis(a2p): 

448 # Returns orthonormal basis (X, Y, Z) and origin (px, py, pz) 

449 loc = a2p.Location 

450 coords = list(loc.Coordinates) 

451 px = float(coords[0]) if len(coords) > 0 else 0.0 

452 py = float(coords[1]) if len(coords) > 1 else 0.0 

453 pz = float(coords[2]) if len(coords) > 2 else 0.0 

454 

455 if a2p.is_a("IfcAxis2Placement3D"): 

456 zdir = (0.0, 0.0, 1.0) 

457 xdir = (1.0, 0.0, 0.0) 

458 if getattr(a2p, "Axis", None): 

459 zvals = _to_float_list(a2p.Axis.DirectionRatios) 

460 zdir = _norm3((zvals + [0.0, 0.0, 0.0])[:3]) 

461 if getattr(a2p, "RefDirection", None): 

462 xvals = _to_float_list(a2p.RefDirection.DirectionRatios) 

463 xdir = _norm3((xvals + [0.0, 0.0, 0.0])[:3]) 

464 ydir = _cross(zdir, xdir) 

465 xdir = _cross(ydir, zdir) 

466 xdir = _norm3(xdir) 

467 ydir = _norm3(ydir) 

468 zdir = _norm3(zdir) 

469 return xdir, ydir, zdir, (px, py, pz) 

470 

471 if a2p.is_a("IfcAxis2Placement2D"): 

472 # 2D placement in XY plane 

473 xdir = (1.0, 0.0, 0.0) 

474 if getattr(a2p, "RefDirection", None): 

475 xvals = _to_float_list(a2p.RefDirection.DirectionRatios) 

476 # Promote 2D to 3D by adding 0 z 

477 xdir = _norm3(((xvals + [0.0])[:2]) + [0.0]) 

478 zdir = (0.0, 0.0, 1.0) 

479 ydir = _cross(zdir, xdir) 

480 xdir = _cross(ydir, zdir) 

481 xdir = _norm3(xdir) 

482 ydir = _norm3(ydir) 

483 return xdir, ydir, zdir, (px, py, pz) 

484 

485 # Fallback identity basis at given location 

486 return (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), (px, py, pz) 

487 

488 

489def _mat_mul(a, b): 

490 r = [[0.0]*4 for _ in range(4)] 

491 for i in range(4): 

492 for j in range(4): 

493 r[i][j] = sum(a[i][k] * b[k][j] for k in range(4)) 

494 return r 

495 

496 

497def _basis_to_matrix(xdir, ydir, zdir, pos): 

498 (xx, xy, xz) = xdir 

499 (yx, yy, yz) = ydir 

500 (zx, zy, zz) = zdir 

501 (tx, ty, tz) = pos 

502 return [ 

503 [xx, yx, zx, tx], 

504 [xy, yy, zy, ty], 

505 [xz, yz, zz, tz], 

506 [0.0, 0.0, 0.0, 1.0], 

507 ] 

508 

509 

510def _local_placement_to_matrix(lpl): 

511 if lpl is None: 

512 return [ 

513 [1.0, 0.0, 0.0, 0.0], 

514 [0.0, 1.0, 0.0, 0.0], 

515 [0.0, 0.0, 1.0, 0.0], 

516 [0.0, 0.0, 0.0, 1.0], 

517 ] 

518 parent_m = _local_placement_to_matrix(getattr(lpl, "PlacementRelTo", None)) 

519 xdir, ydir, zdir, pos = _axis2placement_to_basis(lpl.RelativePlacement) 

520 local_m = _basis_to_matrix(xdir, ydir, zdir, pos) 

521 return _mat_mul(parent_m, local_m) 

522 

523 

524def _matrix_to_trsf(m): 

525 t = gp_Trsf() 

526 t.SetValues( 

527 m[0][0], m[0][1], m[0][2], m[0][3], 

528 m[1][0], m[1][1], m[1][2], m[1][3], 

529 m[2][0], m[2][1], m[2][2], m[2][3] 

530 ) 

531 return t 

532 

533 

534def _product_world_trsf(prod) -> gp_Trsf: 

535 if prod is None: 

536 return gp_Trsf() 

537 lpl = getattr(prod, "ObjectPlacement", None) 

538 m = _local_placement_to_matrix(lpl) 

539 return _matrix_to_trsf(m) 

540 

541 

542# --- Boundary item selection and shape building --- 

543 

544def _pick_connection_item_and_product(rb) -> Tuple[Optional[object], Optional[object]]: 

545 cg = getattr(rb, "ConnectionGeometry", None) 

546 if cg is None: 

547 return None, None 

548 

549 # Prefer geometry on the space side (RelatingSpace). Fallback to RelatedBuildingElement. 

550 if cg.is_a("IfcConnectionSurfaceGeometry"): 

551 if getattr(cg, "SurfaceOnRelatingElement", None): 

552 return cg.SurfaceOnRelatingElement, rb.RelatingSpace 

553 elif getattr(cg, "SurfaceOnRelatedElement", None): 

554 return cg.SurfaceOnRelatedElement, getattr(rb, "RelatedBuildingElement", None) 

555 

556 if cg.is_a("IfcConnectionCurveGeometry"): 

557 if getattr(cg, "CurveOnRelatingElement", None): 

558 return cg.CurveOnRelatingElement, rb.RelatingSpace 

559 elif getattr(cg, "CurveOnRelatedElement", None): 

560 return cg.CurveOnRelatedElement, getattr(rb, "RelatedBuildingElement", None) 

561 

562 return None, None 

563 

564 

565def build_space_boundary_shapes(ifc): 

566 """Create TopoDS_Shapes for IfcRelSpaceBoundary connection geometry, transformed to world.""" 

567 gs = ifcopenshell.geom.settings() 

568 # Ensure OCC shapes and curves are included (for curve-based boundaries) 

569 gs.set(gs.USE_PYTHON_OPENCASCADE, True) 

570 gs.set(gs.USE_WORLD_COORDS, True) 

571 gs.set( 

572 "dimensionality", 

573 ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) # 2 

574 shapes = [] 

575 

576 # Collect all boundary subtypes 

577 boundaries = [] 

578 for t in ("IfcRelSpaceBoundary2ndLevel", "IfcRelSpaceBoundary1stLevel", 

579 "IfcRelSpaceBoundary"): 

580 try: 

581 boundaries.extend(ifc.by_type(t)) 

582 except Exception: 

583 pass 

584 

585 for rb in boundaries: 

586 item, prod = _pick_connection_item_and_product(rb) 

587 if not item: 

588 continue 

589 try: 

590 if item.InnerBoundaries: 

591 inners = [] 

592 

593 for inn in item.InnerBoundaries: 

594 ifc_new = ifcopenshell.file() 

595 temp_sore = ifc_new.create_entity('IfcCurveBoundedPlane', 

596 OuterBoundary=inn, 

597 BasisSurface=item.BasisSurface) 

598 temp_sore.InnerBoundaries = () 

599 compound = ifcopenshell.geom.create_shape(gs, temp_sore) 

600 faces = PyOCCTools.get_face_from_shape(compound) 

601 inners.append(faces) 

602 item.InnerBoundaries = () 

603 outer_shape_data = ifcopenshell.geom.create_shape(gs, item) 

604 shape_data = PyOCCTools.triangulate_bound_shape(outer_shape_data, inners) 

605 else: 

606 shape_data = ifcopenshell.geom.create_shape(gs, item) 

607 

608 except Exception as ex: 

609 print(f"Exception {ex} for {rb}!") 

610 continue 

611 occ_shape = shape_data # TopoDS_Shape 

612 tr = _product_world_trsf(prod) 

613 tr_shape = BRepBuilderAPI_Transform(occ_shape, tr, True).Shape() 

614 # tr_shape = heal_boundary_shape(tr_shape) # <-- shape healing 

615 shapes.append([rb.GlobalId, tr_shape]) 

616 

617 return shapes 

618 

619 

620def main(): 

621 # Choose file path from CLI or dialog 

622 

623 path = "path/to/ifc/file" 

624 

625 try: 

626 ifc = ifcopenshell.open(path) 

627 except Exception as e: 

628 print(f"Failed to open IFC: {e}") 

629 return 

630 

631 shapes = build_space_boundary_shapes(ifc) 

632 print(f"Built {len(shapes)} space boundary shapes.") 

633 

634 # Optional quick display 

635 if shapes: 

636 display_occ_shapes(ifc, shapes) 

637 else: 

638 print("No drawable connection geometry found for space boundaries.") 

639 

640 

641if __name__ == "__main__": 

642 main()