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
« 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).
6import sys
7from typing import List, Tuple, Optional
9import ifcopenshell
10import ifcopenshell.geom
12from OCC.Core.TopoDS import TopoDS_Shape
13from OCC.Core.gp import gp_Trsf
14from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
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
32from bim2sim.utilities.pyocc_tools import PyOCCTools
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
56 faces = []
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
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
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)
113 def _dist(a, b):
114 return gp_Vec(a, b).Magnitude()
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()
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
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
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
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])
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)
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)
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
197 # Ensure CCW orientation (optional)
198 if _area(pts) < 0:
199 pts.reverse()
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
220 # Close loop
221 pts.append(pts[0])
222 return pts
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
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
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)
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
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
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
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
351 return None
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
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
384 # Fallback
385 return shape
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()
429# --- Placement utilities (IfcLocalPlacement -> gp_Trsf) ---
431def _to_float_list(seq):
432 return [float(x) for x in seq]
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)
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)
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
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)
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)
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)
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
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 ]
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)
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
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)
542# --- Boundary item selection and shape building ---
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
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)
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)
562 return None, None
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 = []
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
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 = []
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)
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])
617 return shapes
620def main():
621 # Choose file path from CLI or dialog
623 path = "path/to/ifc/file"
625 try:
626 ifc = ifcopenshell.open(path)
627 except Exception as e:
628 print(f"Failed to open IFC: {e}")
629 return
631 shapes = build_space_boundary_shapes(ifc)
632 print(f"Built {len(shapes)} space boundary shapes.")
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.")
641if __name__ == "__main__":
642 main()