mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
7cd40bf8cb
ifcquery is a new command-line tool for querying and inspecting IFC models. All output is JSON.
Subcommands:
summary — schema version, entity counts, project metadata
tree — full spatial hierarchy (Project → Site → Building → Storeys → Spaces → Elements)
info <id> — deep inspection of any entity by step ID (attributes, psets, placement matrix, type, material)
select <query> — filter elements using ifcopenshell selector syntax
relations <id> — relationships for an element; --traverse up walks to IfcProject
clash <id> — geometric intersection and clearance detection
validate — schema/constraint validation; --rules adds EXPRESS checks
schedule — work schedules with nested task trees
cost — cost schedules with nested cost item trees
schema <class> — IFC class documentation from the model's schema version
plot — SVG plan drawing
render — 3D geometry rendering
contexts — geometric representation contexts
materials — material assignments
Usage:
python3 -m ifcquery <file.ifc> <subcommand> [args]
Generated with the assistance of an AI coding tool.
356 lines
16 KiB
Python
356 lines
16 KiB
Python
# This file was generated with the assistance of an AI coding tool.
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
import ifcopenshell
|
|
import ifcopenshell.api.aggregate
|
|
import ifcopenshell.api.context
|
|
import ifcopenshell.api.geometry
|
|
import ifcopenshell.api.owner.settings
|
|
import ifcopenshell.api.project
|
|
import ifcopenshell.api.root
|
|
import ifcopenshell.api.spatial
|
|
import ifcopenshell.api.unit
|
|
import ifcopenshell.guid
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from ifcquery.render import _make_profile_occurrence, _make_type_occurrence, render
|
|
|
|
try:
|
|
import pyvista # noqa: F401
|
|
|
|
HAS_PYVISTA = True
|
|
except ImportError:
|
|
HAS_PYVISTA = False
|
|
|
|
pytestmark = pytest.mark.skipif(not HAS_PYVISTA, reason="pyvista not installed")
|
|
|
|
PNG_MAGIC = b"\x89PNG"
|
|
|
|
|
|
@pytest.fixture
|
|
def model_with_geometry():
|
|
"""Create an IFC4 model with walls that have geometric representations."""
|
|
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="TestProject")
|
|
ifcopenshell.api.unit.assign_unit(f)
|
|
|
|
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="TestSite")
|
|
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="TestBuilding")
|
|
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
|
|
|
|
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
|
|
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
|
|
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
|
|
|
|
model_ctx = ifcopenshell.api.context.add_context(f, context_type="Model")
|
|
body = ifcopenshell.api.context.add_context(
|
|
f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model_ctx
|
|
)
|
|
|
|
wall1 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall001")
|
|
rep1 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
|
|
ifcopenshell.api.geometry.assign_representation(f, product=wall1, representation=rep1)
|
|
ifcopenshell.api.spatial.assign_container(f, products=[wall1], relating_structure=storey)
|
|
|
|
wall2 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall002")
|
|
rep2 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=4, height=3, thickness=0.2)
|
|
ifcopenshell.api.geometry.assign_representation(f, product=wall2, representation=rep2)
|
|
ifcopenshell.api.spatial.assign_container(f, products=[wall2], relating_structure=storey)
|
|
matrix2 = np.eye(4)
|
|
matrix2[1, 3] = 3.0
|
|
ifcopenshell.api.geometry.edit_object_placement(f, product=wall2, matrix=matrix2)
|
|
|
|
return f
|
|
|
|
|
|
@pytest.fixture
|
|
def library_with_type():
|
|
"""IFC4 library file: a WallType with a RepresentationMap but no instances."""
|
|
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="LibProject")
|
|
ifcopenshell.api.unit.assign_unit(f)
|
|
|
|
model_ctx = ifcopenshell.api.context.add_context(f, context_type="Model")
|
|
body = ifcopenshell.api.context.add_context(
|
|
f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model_ctx
|
|
)
|
|
|
|
# Build the shape representation and wrap it in an IfcRepresentationMap.
|
|
shape_rep = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=3, height=2.5, thickness=0.2)
|
|
origin = f.create_entity("IfcCartesianPoint", Coordinates=(0.0, 0.0, 0.0))
|
|
z_dir = f.create_entity("IfcDirection", DirectionRatios=(0.0, 0.0, 1.0))
|
|
x_dir = f.create_entity("IfcDirection", DirectionRatios=(1.0, 0.0, 0.0))
|
|
map_origin = f.create_entity("IfcAxis2Placement3D", Location=origin, Axis=z_dir, RefDirection=x_dir)
|
|
rep_map = f.create_entity("IfcRepresentationMap", MappingOrigin=map_origin, MappedRepresentation=shape_rep)
|
|
|
|
wall_type = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="LibWallType")
|
|
wall_type.RepresentationMaps = [rep_map]
|
|
|
|
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)
|
|
assert isinstance(result, bytes)
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
def test_iso_view(self, model_with_geometry):
|
|
result = render(model_with_geometry, view="iso")
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
def test_top_view(self, model_with_geometry):
|
|
result = render(model_with_geometry, view="top")
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
def test_south_view(self, model_with_geometry):
|
|
result = render(model_with_geometry, view="south")
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
def test_unknown_view_falls_back_to_iso(self, model_with_geometry):
|
|
# Unknown view strings fall through to isometric
|
|
result = render(model_with_geometry, view="diagonal")
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
|
|
class TestRenderSelector:
|
|
def test_selector_restricts_elements(self, model_with_geometry):
|
|
result = render(model_with_geometry, selector="IfcWall")
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
def test_selector_no_match_raises(self, model_with_geometry):
|
|
with pytest.raises(ValueError, match="matched no elements"):
|
|
render(model_with_geometry, selector="IfcDoor")
|
|
|
|
|
|
class TestRenderHighlight:
|
|
def test_highlight_single_element(self, model_with_geometry):
|
|
wall = model_with_geometry.by_type("IfcWall")[0]
|
|
result = render(model_with_geometry, element_ids=[wall.id()])
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
def test_highlight_multiple_elements(self, model_with_geometry):
|
|
walls = model_with_geometry.by_type("IfcWall")
|
|
result = render(model_with_geometry, element_ids=[w.id() for w in walls])
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
|
|
class TestRenderTypes:
|
|
def test_render_type_by_selector(self, library_with_type):
|
|
"""Selecting a type class renders its RepresentationMap geometry."""
|
|
model, wall_type = library_with_type
|
|
result = render(model, selector="IfcWallType")
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
def test_render_type_by_element_id(self, library_with_type):
|
|
"""Passing a type step-ID via element_ids renders it highlighted."""
|
|
model, wall_type = library_with_type
|
|
result = render(model, element_ids=[wall_type.id()])
|
|
assert result[:4] == PNG_MAGIC
|
|
|
|
def test_original_model_unmodified(self, library_with_type):
|
|
"""Rendering a type must not add entities to the original model."""
|
|
model, wall_type = library_with_type
|
|
entity_count_before = len(list(model))
|
|
render(model, selector="IfcWallType")
|
|
assert len(list(model)) == entity_count_before
|
|
|
|
def test_make_type_occurrence_no_rep_maps(self, library_with_type):
|
|
"""_make_type_occurrence returns None for a type with no RepresentationMaps."""
|
|
model, _ = library_with_type
|
|
bare_type = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWallType", name="Bare")
|
|
assert _make_type_occurrence(model, bare_type) is None
|
|
|
|
def test_type_without_rep_maps_raises(self):
|
|
"""Selecting a type that has no RepresentationMaps raises ValueError."""
|
|
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]
|
|
ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="P")
|
|
ifcopenshell.api.unit.assign_unit(f)
|
|
ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="Bare")
|
|
with pytest.raises(ValueError):
|
|
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."""
|
|
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="P")
|
|
ifcopenshell.api.unit.assign_unit(f)
|
|
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="S")
|
|
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="B")
|
|
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="GF")
|
|
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
|
|
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
|
|
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
|
|
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wallless")
|
|
ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=storey)
|
|
|
|
with pytest.raises(ValueError, match="No renderable geometry"):
|
|
render(f)
|
|
|
|
|
|
class TestCLI:
|
|
@staticmethod
|
|
def _ifc_path(model):
|
|
f = tempfile.NamedTemporaryFile(suffix=".ifc", delete=False)
|
|
model.write(f.name)
|
|
f.close()
|
|
return f.name
|
|
|
|
def test_render_writes_png(self, model_with_geometry):
|
|
ifc_path = self._ifc_path(model_with_geometry)
|
|
out_path = ifc_path.replace(".ifc", "_out.png")
|
|
try:
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "ifcquery", ifc_path, "render", "-o", out_path],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
assert os.path.exists(out_path)
|
|
with open(out_path, "rb") as f:
|
|
assert f.read(4) == PNG_MAGIC
|
|
finally:
|
|
for path in (ifc_path, out_path):
|
|
try:
|
|
os.unlink(path)
|
|
except OSError:
|
|
pass
|
|
|
|
def test_render_default_output_path(self, model_with_geometry):
|
|
ifc_path = self._ifc_path(model_with_geometry)
|
|
expected_png = ifc_path.replace(".ifc", ".png")
|
|
try:
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "ifcquery", ifc_path, "render"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
assert os.path.exists(expected_png)
|
|
finally:
|
|
for path in (ifc_path, expected_png):
|
|
try:
|
|
os.unlink(path)
|
|
except OSError:
|
|
pass
|
|
|
|
def test_render_with_selector(self, model_with_geometry):
|
|
ifc_path = self._ifc_path(model_with_geometry)
|
|
out_path = ifc_path.replace(".ifc", "_sel.png")
|
|
try:
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "ifcquery", ifc_path, "render", "-o", out_path, "--selector", "IfcWall"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
with open(out_path, "rb") as f:
|
|
assert f.read(4) == PNG_MAGIC
|
|
finally:
|
|
for path in (ifc_path, out_path):
|
|
try:
|
|
os.unlink(path)
|
|
except OSError:
|
|
pass
|
|
|
|
def test_render_with_view(self, model_with_geometry):
|
|
ifc_path = self._ifc_path(model_with_geometry)
|
|
out_path = ifc_path.replace(".ifc", "_top.png")
|
|
try:
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "ifcquery", ifc_path, "render", "-o", out_path, "--view", "top"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
with open(out_path, "rb") as f:
|
|
assert f.read(4) == PNG_MAGIC
|
|
finally:
|
|
for path in (ifc_path, out_path):
|
|
try:
|
|
os.unlink(path)
|
|
except OSError:
|
|
pass
|