mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
ifcquery: render types with material profile sets
Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -231,6 +231,102 @@ def _make_type_occurrence(model: ifcopenshell.file, type_entity) -> object | Non
|
||||
return occurrence
|
||||
|
||||
|
||||
def _make_profile_occurrence(model: ifcopenshell.file, type_entity) -> object | None:
|
||||
"""Create a temporary occurrence for a type that has a material profile set.
|
||||
|
||||
Finds the first profile in the type's IfcMaterialProfileSet and creates a
|
||||
1-metre IfcExtrudedAreaSolid body representation from it. Returns the
|
||||
occurrence, or ``None`` when no usable profile is found.
|
||||
|
||||
.. note::
|
||||
Intended for use on a temporary model copy; caller discards it after
|
||||
rendering.
|
||||
"""
|
||||
# Locate the first profile from the type's material profile set.
|
||||
profile = None
|
||||
for rel in getattr(type_entity, "HasAssociations", []):
|
||||
if not rel.is_a("IfcRelAssociatesMaterial"):
|
||||
continue
|
||||
mat = rel.RelatingMaterial
|
||||
if mat.is_a("IfcMaterialProfileSetUsage"):
|
||||
mat = mat.ForProfileSet
|
||||
if mat.is_a("IfcMaterialProfileSet"):
|
||||
mat_profiles = list(getattr(mat, "MaterialProfiles", None) or [])
|
||||
if mat_profiles:
|
||||
profile = getattr(mat_profiles[0], "Profile", None)
|
||||
if profile is not None:
|
||||
break
|
||||
if profile is None:
|
||||
return None
|
||||
|
||||
# Find a Body subcontext, or fall back to any Model context.
|
||||
body_ctx = None
|
||||
for ctx in model.by_type("IfcGeometricRepresentationSubContext"):
|
||||
if ctx.ContextIdentifier == "Body":
|
||||
body_ctx = ctx
|
||||
break
|
||||
if body_ctx is None:
|
||||
for ctx in model.by_type("IfcGeometricRepresentationContext"):
|
||||
if ctx.ContextType == "Model":
|
||||
body_ctx = ctx
|
||||
break
|
||||
if body_ctx is None:
|
||||
return None
|
||||
|
||||
# Extrude 1 metre along Z (profile lies in XY plane).
|
||||
origin = model.create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0))
|
||||
z_axis = model.create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0))
|
||||
x_axis = model.create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0))
|
||||
position = model.create_entity(
|
||||
"IfcAxis2Placement3D", Location=origin, Axis=z_axis, RefDirection=x_axis
|
||||
)
|
||||
extrude_dir = model.create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0))
|
||||
extrusion = model.create_entity(
|
||||
"IfcExtrudedAreaSolid",
|
||||
SweptArea=profile,
|
||||
Position=position,
|
||||
ExtrudedDirection=extrude_dir,
|
||||
Depth=1.0,
|
||||
)
|
||||
shape_rep = model.create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
ContextOfItems=body_ctx,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="SweptSolid",
|
||||
Items=[extrusion],
|
||||
)
|
||||
prod_def_shape = model.create_entity(
|
||||
"IfcProductDefinitionShape",
|
||||
Representations=[shape_rep],
|
||||
)
|
||||
|
||||
# Identity placement.
|
||||
pt = model.create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0))
|
||||
z_dir = model.create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0))
|
||||
x_dir = model.create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0))
|
||||
axis2 = model.create_entity("IfcAxis2Placement3D", Location=pt, Axis=z_dir, RefDirection=x_dir)
|
||||
placement = model.create_entity("IfcLocalPlacement", RelativePlacement=axis2)
|
||||
|
||||
occ_class = _get_occurrence_class(type_entity)
|
||||
try:
|
||||
occurrence = model.create_entity(
|
||||
occ_class,
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
Name=f"_profile_preview_{type_entity.id()}",
|
||||
ObjectPlacement=placement,
|
||||
Representation=prod_def_shape,
|
||||
)
|
||||
except Exception:
|
||||
occurrence = model.create_entity(
|
||||
"IfcBuildingElementProxy",
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
Name=f"_profile_preview_{type_entity.id()}",
|
||||
ObjectPlacement=placement,
|
||||
Representation=prod_def_shape,
|
||||
)
|
||||
return occurrence
|
||||
|
||||
|
||||
def _render_with_types(
|
||||
model: ifcopenshell.file,
|
||||
types: list,
|
||||
@@ -256,12 +352,12 @@ def _render_with_types(
|
||||
type_id_to_occ_id: dict[int, int] = {}
|
||||
for t in types:
|
||||
tmp_type = tmp.by_id(t.id())
|
||||
occ = _make_type_occurrence(tmp, tmp_type)
|
||||
occ = _make_type_occurrence(tmp, tmp_type) or _make_profile_occurrence(tmp, tmp_type)
|
||||
if occ:
|
||||
type_id_to_occ_id[t.id()] = occ.id()
|
||||
|
||||
if not type_id_to_occ_id:
|
||||
raise ValueError("Type entities have no RepresentationMaps to render")
|
||||
raise ValueError("Type entities have no RepresentationMaps or material profile sets to render")
|
||||
|
||||
include = [tmp.by_id(occ_id) for occ_id in type_id_to_occ_id.values()]
|
||||
if selector_elements:
|
||||
|
||||
@@ -6,6 +6,7 @@ import tempfile
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.aggregate
|
||||
import ifcopenshell.guid
|
||||
import ifcopenshell.api.context
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.api.owner.settings
|
||||
@@ -16,7 +17,7 @@ import ifcopenshell.api.unit
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from ifcquery.render import render, _make_type_occurrence
|
||||
from ifcquery.render import render, _make_type_occurrence, _make_profile_occurrence
|
||||
|
||||
try:
|
||||
import pyvista # noqa: F401
|
||||
@@ -98,6 +99,44 @@ def library_with_type():
|
||||
return f, wall_type
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library_with_profile_type():
|
||||
"""IFC4 library: a BeamType with an IfcMaterialProfileSet but no RepresentationMaps."""
|
||||
f = ifcopenshell.api.project.create_file()
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
|
||||
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
|
||||
|
||||
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="ProfileLibProject")
|
||||
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
|
||||
)
|
||||
|
||||
# Rectangular profile 0.2m x 0.3m
|
||||
profile = f.create_entity(
|
||||
"IfcRectangleProfileDef",
|
||||
ProfileType="AREA",
|
||||
ProfileName="200x300",
|
||||
XDim=0.2,
|
||||
YDim=0.3,
|
||||
)
|
||||
material = f.create_entity("IfcMaterial", Name="Steel")
|
||||
mat_profile = f.create_entity("IfcMaterialProfile", Material=material, Profile=profile)
|
||||
profile_set = f.create_entity("IfcMaterialProfileSet", MaterialProfiles=[mat_profile])
|
||||
|
||||
beam_type = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBeamType", name="200x300 Steel Beam")
|
||||
rel = f.create_entity(
|
||||
"IfcRelAssociatesMaterial",
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
RelatedObjects=[beam_type],
|
||||
RelatingMaterial=profile_set,
|
||||
)
|
||||
|
||||
return f, beam_type
|
||||
|
||||
|
||||
class TestRenderBasic:
|
||||
def test_returns_png_bytes(self, model_with_geometry):
|
||||
result = render(model_with_geometry)
|
||||
@@ -182,6 +221,34 @@ class TestRenderTypes:
|
||||
render(f, selector="IfcWallType")
|
||||
|
||||
|
||||
class TestRenderProfileTypes:
|
||||
def test_render_profile_type_by_element_id(self, library_with_profile_type):
|
||||
"""A type with only a material profile set renders via temporary extrusion."""
|
||||
model, beam_type = library_with_profile_type
|
||||
result = render(model, element_ids=[beam_type.id()])
|
||||
assert result[:4] == PNG_MAGIC
|
||||
|
||||
def test_make_profile_occurrence_creates_occurrence(self, library_with_profile_type):
|
||||
"""_make_profile_occurrence returns an occurrence entity for a profile-set type."""
|
||||
model, beam_type = library_with_profile_type
|
||||
occ = _make_profile_occurrence(model, beam_type)
|
||||
assert occ is not None
|
||||
|
||||
def test_make_profile_occurrence_no_profile_returns_none(self, library_with_type):
|
||||
"""_make_profile_occurrence returns None when type has no material profile set."""
|
||||
model, wall_type = library_with_type
|
||||
# wall_type has RepresentationMaps but no material profile set
|
||||
occ = _make_profile_occurrence(model, wall_type)
|
||||
assert occ is None
|
||||
|
||||
def test_original_model_unmodified_for_profile_type(self, library_with_profile_type):
|
||||
"""Rendering a profile-based type does not modify the original model."""
|
||||
model, beam_type = library_with_profile_type
|
||||
entity_count_before = len(list(model))
|
||||
render(model, element_ids=[beam_type.id()])
|
||||
assert len(list(model)) == entity_count_before
|
||||
|
||||
|
||||
class TestRenderNoGeometry:
|
||||
def test_no_geometry_raises(self):
|
||||
"""A model without geometry representations raises ValueError."""
|
||||
|
||||
Reference in New Issue
Block a user