mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user