ifcquery info: add geometry_summary for Body representations

When an element has a Body representation, ifc_info now includes a
geometry_summary key with representation type and geometry details.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Bruno Postle
2026-03-20 22:57:32 +00:00
parent a95c9fcaab
commit b45cbefdd9
2 changed files with 215 additions and 0 deletions
+144
View File
@@ -25,6 +25,145 @@ import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.placement
# ---------------------------------------------------------------------------
# Geometry summary helpers
# ---------------------------------------------------------------------------
_MAX_PROFILE_POINTS = 20
def _rc(coords) -> list[float]:
"""Round a coordinate sequence to 6 decimal places."""
return [round(float(c), 6) for c in coords]
def _curve_points(curve) -> list | None:
if curve.is_a("IfcPolyline"):
return [_rc(p.Coordinates) for p in curve.Points]
if curve.is_a("IfcIndexedPolyCurve"):
return [_rc(c) for c in curve.Points.CoordList]
return None
def _profile_summary(profile) -> dict:
t = profile.is_a()
result: dict[str, Any] = {"type": t}
if t == "IfcRectangleProfileDef":
result["x_dim"] = profile.XDim
result["y_dim"] = profile.YDim
elif t in ("IfcCircleProfileDef", "IfcCircleHollowProfileDef"):
result["radius"] = profile.Radius
if t == "IfcCircleHollowProfileDef":
result["wall_thickness"] = profile.WallThickness
elif t in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"):
pts = _curve_points(profile.OuterCurve)
if pts is not None:
if len(pts) <= _MAX_PROFILE_POINTS:
result["points"] = pts
else:
result["point_count"] = len(pts)
elif t == "IfcCompositeProfileDef":
result["profiles"] = [_profile_summary(p) for p in profile.Profiles]
return result
def _half_space_plane(half_space) -> dict | None:
if not half_space.is_a("IfcHalfSpaceSolid"):
return None
surface = half_space.BaseSurface
if not surface or not surface.is_a("IfcPlane"):
return None
pos = surface.Position
loc = _rc(pos.Location.Coordinates)
normal = _rc(pos.Axis.DirectionRatios) if pos.Axis else [0.0, 0.0, 1.0]
return {"location": loc, "normal": normal}
def _walk_clipping(item) -> tuple:
"""Return (base_solid, [clipping_plane_dicts]) from a BooleanClippingResult chain."""
planes = []
current = item
while current.is_a("IfcBooleanClippingResult"):
plane = _half_space_plane(current.SecondOperand)
if plane:
planes.append(plane)
current = current.FirstOperand
return current, planes
def _swept_solid_dict(item) -> dict:
result: dict[str, Any] = {"solid_type": item.is_a()}
if item.is_a("IfcExtrudedAreaSolid"):
result["depth"] = item.Depth
if item.ExtrudedDirection:
result["direction"] = _rc(item.ExtrudedDirection.DirectionRatios)
if item.SweptArea:
result["profile"] = _profile_summary(item.SweptArea)
return result
def _summarize_rep(rep) -> dict:
rep_type = rep.RepresentationType or ""
result: dict[str, Any] = {"representation_type": rep_type}
items = list(rep.Items)
if rep_type == "MappedRepresentation":
for item in items:
if item.is_a("IfcMappedItem"):
return _summarize_rep(item.MappingSource.MappedRepresentation)
elif rep_type == "SweptSolid":
result["solids"] = [_swept_solid_dict(item) for item in items]
elif rep_type == "Clipping":
solids = []
for item in items:
base, planes = _walk_clipping(item)
solid = _swept_solid_dict(base)
if planes:
solid["clipping_planes"] = planes
solids.append(solid)
result["solids"] = solids
elif rep_type == "CSG":
ops = []
for item in items:
if hasattr(item, "Operator"):
ops.append({"operator": str(item.Operator), "type": item.is_a()})
if ops:
result["operations"] = ops
elif rep_type in ("Brep", "Tessellation", "SolidModel"):
face_count = 0
vertex_count = 0
for item in items:
if item.is_a("IfcPolygonalFaceSet"):
face_count += len(item.Faces)
vertex_count += len(item.Coordinates.CoordList)
elif item.is_a("IfcFacetedBrep"):
face_count += len(item.Outer.CfsFaces)
if face_count:
result["face_count"] = face_count
if vertex_count:
result["vertex_count"] = vertex_count
return result
def _geometry_summary(element) -> dict | None:
if not hasattr(element, "Representation") or not element.Representation:
return None
body_rep = next(
(r for r in element.Representation.Representations if r.RepresentationIdentifier == "Body"),
None,
)
if body_rep is None:
return None
try:
return _summarize_rep(body_rep)
except Exception:
return None
def _serialize_attribute(value: Any) -> Any:
"""Convert an IFC attribute value to a JSON-serializable form."""
@@ -115,4 +254,9 @@ def info(model: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dic
except Exception:
pass
# Geometry summary
geom = _geometry_summary(element)
if geom:
result["geometry_summary"] = geom
return result
+71
View File
@@ -1,4 +1,12 @@
# This file was generated with the assistance of an AI coding tool.
import ifcopenshell
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.project
import ifcopenshell.api.root
import ifcopenshell.api.unit
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
from ifcquery.info import info
@@ -30,3 +38,66 @@ class TestInfo:
result = info(model, wall)
# Should not raise
json.dumps(result)
def test_no_geometry_summary_without_representation(self, model):
wall = model.by_type("IfcWall")[0]
result = info(model, wall)
assert "geometry_summary" not in result
class TestGeometrySummary:
def _make_model_with_wall(self):
f = ifcopenshell.api.project.create_file()
ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject")
ifcopenshell.api.unit.assign_unit(f)
model_ctx = ifcopenshell.api.context.add_context(f, context_type="Model")
ifcopenshell.api.context.add_context(
f, context_type="Model", context_identifier="Body",
target_view="MODEL_VIEW", parent=model_ctx,
)
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="W1")
ifcopenshell.api.geometry.edit_object_placement(f, product=wall)
return f, wall
def _body_context(self, f):
return ifcopenshell.util.representation.get_context(f, "Model", "Body", "MODEL_VIEW")
def test_swept_solid_summary(self):
f, wall = self._make_model_with_wall()
body = self._body_context(f)
rep = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5.0, height=3.0, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall, representation=rep)
result = info(f, wall)
gs = result["geometry_summary"]
assert gs["representation_type"] == "SweptSolid"
assert len(gs["solids"]) == 1
solid = gs["solids"][0]
assert solid["depth"] == 3000.0 # stored in project units (mm)
assert solid["profile"]["type"] == "IfcArbitraryClosedProfileDef"
assert len(solid["profile"]["points"]) == 5 # closed polyline
def test_clipping_summary(self):
f, wall = self._make_model_with_wall()
body = self._body_context(f)
rep = ifcopenshell.api.geometry.add_wall_representation(
f, context=body, length=5.0, height=4.0, thickness=0.2,
clippings=[{"location": (0.0, 0.0, 3.0), "normal": (0.0, 0.0, 1.0)}],
)
ifcopenshell.api.geometry.assign_representation(f, product=wall, representation=rep)
result = info(f, wall)
gs = result["geometry_summary"]
assert gs["representation_type"] == "Clipping"
solid = gs["solids"][0]
assert len(solid["clipping_planes"]) == 1
plane = solid["clipping_planes"][0]
assert plane["location"][2] == 3000.0 # stored in project units (mm)
assert plane["normal"] == [0.0, 0.0, 1.0]
def test_geometry_summary_json_serializable(self):
import json
f, wall = self._make_model_with_wall()
body = self._body_context(f)
rep = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5.0, height=3.0, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall, representation=rep)
result = info(f, wall)
json.dumps(result)