mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-31 17:06:29 +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.element
|
||||||
import ifcopenshell.util.placement
|
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:
|
def _serialize_attribute(value: Any) -> Any:
|
||||||
"""Convert an IFC attribute value to a JSON-serializable form."""
|
"""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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Geometry summary
|
||||||
|
geom = _geometry_summary(element)
|
||||||
|
if geom:
|
||||||
|
result["geometry_summary"] = geom
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
# This file was generated with the assistance of an AI coding tool.
|
# 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
|
from ifcquery.info import info
|
||||||
|
|
||||||
|
|
||||||
@@ -30,3 +38,66 @@ class TestInfo:
|
|||||||
result = info(model, wall)
|
result = info(model, wall)
|
||||||
# Should not raise
|
# Should not raise
|
||||||
json.dumps(result)
|
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