mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 23:36:20 +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
|
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(
|
def _render_with_types(
|
||||||
model: ifcopenshell.file,
|
model: ifcopenshell.file,
|
||||||
types: list,
|
types: list,
|
||||||
@@ -256,12 +352,12 @@ def _render_with_types(
|
|||||||
type_id_to_occ_id: dict[int, int] = {}
|
type_id_to_occ_id: dict[int, int] = {}
|
||||||
for t in types:
|
for t in types:
|
||||||
tmp_type = tmp.by_id(t.id())
|
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:
|
if occ:
|
||||||
type_id_to_occ_id[t.id()] = occ.id()
|
type_id_to_occ_id[t.id()] = occ.id()
|
||||||
|
|
||||||
if not type_id_to_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()]
|
include = [tmp.by_id(occ_id) for occ_id in type_id_to_occ_id.values()]
|
||||||
if selector_elements:
|
if selector_elements:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import tempfile
|
|||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api.aggregate
|
import ifcopenshell.api.aggregate
|
||||||
|
import ifcopenshell.guid
|
||||||
import ifcopenshell.api.context
|
import ifcopenshell.api.context
|
||||||
import ifcopenshell.api.geometry
|
import ifcopenshell.api.geometry
|
||||||
import ifcopenshell.api.owner.settings
|
import ifcopenshell.api.owner.settings
|
||||||
@@ -16,7 +17,7 @@ import ifcopenshell.api.unit
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ifcquery.render import render, _make_type_occurrence
|
from ifcquery.render import render, _make_type_occurrence, _make_profile_occurrence
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pyvista # noqa: F401
|
import pyvista # noqa: F401
|
||||||
@@ -98,6 +99,44 @@ def library_with_type():
|
|||||||
return f, wall_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:
|
class TestRenderBasic:
|
||||||
def test_returns_png_bytes(self, model_with_geometry):
|
def test_returns_png_bytes(self, model_with_geometry):
|
||||||
result = render(model_with_geometry)
|
result = render(model_with_geometry)
|
||||||
@@ -182,6 +221,34 @@ class TestRenderTypes:
|
|||||||
render(f, selector="IfcWallType")
|
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:
|
class TestRenderNoGeometry:
|
||||||
def test_no_geometry_raises(self):
|
def test_no_geometry_raises(self):
|
||||||
"""A model without geometry representations raises ValueError."""
|
"""A model without geometry representations raises ValueError."""
|
||||||
|
|||||||
Reference in New Issue
Block a user