mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Add ifcmcp ifc_render tool and ifcquery render subcommand
Renders IFC model geometry to a PNG image using pyvista (off_screen).
ifcquery CLI:
ifcquery <file> render [-o out.png] [--selector QUERY]
[--element ID[,ID...]] [--view iso|top|south|north|east|west]
ifcmcp MCP tool:
ifc_render(selector, element_ids, view) -> list[ImageContent]
Returns base64-encoded PNG as an MCP ImageContent block so the agent
can inspect the model geometry inline.
Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -12,7 +12,7 @@ from ifcedit.quantify import run_quantify
|
||||
from ifcedit.run import run_api
|
||||
from ifcquery import clash as clash_mod
|
||||
from ifcquery import cost as cost_mod
|
||||
from ifcquery import info, relations, schedule, schema, select, summary, tree
|
||||
from ifcquery import info, relations, render as render_mod, schedule, schema, select, summary, tree
|
||||
from ifcquery import validate as validate_mod
|
||||
|
||||
|
||||
@@ -211,6 +211,30 @@ class IfcSession:
|
||||
"""Return IFC class documentation for entity_type using the model's schema version."""
|
||||
return schema.schema(self._require_model(), entity_type)
|
||||
|
||||
def ifc_render(
|
||||
self,
|
||||
selector: str = "",
|
||||
element_ids: list[int] | None = None,
|
||||
view: str = "iso",
|
||||
) -> bytes:
|
||||
"""Render the loaded model to a PNG image and return raw bytes.
|
||||
|
||||
:param selector: ifcopenshell selector to restrict rendered elements
|
||||
(e.g. ``'IfcWall'``). Omit to render the whole model.
|
||||
:param element_ids: Step IDs of elements to highlight. Other elements
|
||||
are rendered in translucent grey.
|
||||
:param view: Camera angle: ``iso``, ``top``, ``south``, ``north``,
|
||||
``east``, or ``west``.
|
||||
:return: PNG image as raw bytes.
|
||||
"""
|
||||
model = self._require_model()
|
||||
return render_mod.render(
|
||||
model,
|
||||
selector=selector if selector else None,
|
||||
element_ids=element_ids,
|
||||
view=view,
|
||||
)
|
||||
|
||||
def ifc_quantify(self, rule: str, selector: str = "") -> dict[str, Any]:
|
||||
"""Run quantity take-off on the model using the named rule.
|
||||
|
||||
@@ -396,4 +420,28 @@ class IfcSession:
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "ifc_render",
|
||||
"description": (
|
||||
"Render the loaded IFC model to a PNG image for visual inspection. "
|
||||
"Use selector to restrict which elements are rendered (e.g. a single storey). "
|
||||
"Use element_ids to highlight elements against a greyed-out background. "
|
||||
"Returns base64-encoded PNG bytes."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"selector": {"type": "string", "description": "ifcopenshell selector (default: whole model)"},
|
||||
"element_ids": {"type": "array", "items": {"type": "integer"}, "description": "Step IDs of elements to highlight"},
|
||||
"view": {
|
||||
"type": "string",
|
||||
"enum": ["iso", "top", "south", "north", "east", "west"],
|
||||
"description": "Camera angle (default: iso)",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,14 +1,17 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from typing import Any
|
||||
|
||||
from ifcmcp.core import IfcSession
|
||||
|
||||
try:
|
||||
from mcp.server.fastmcp import FastMCP # type: ignore
|
||||
from mcp.types import ImageContent # type: ignore
|
||||
except Exception: # pragma: no cover
|
||||
FastMCP = None # type: ignore
|
||||
ImageContent = None # type: ignore
|
||||
|
||||
|
||||
def build_server() -> Any:
|
||||
@@ -116,4 +119,26 @@ def build_server() -> Any:
|
||||
def ifc_quantify(rule: str, selector: str = "") -> dict[str, Any]:
|
||||
return session.ifc_quantify(rule=rule, selector=selector)
|
||||
|
||||
@server.tool(structured_output=False)
|
||||
def ifc_render(
|
||||
selector: str = "",
|
||||
element_ids: list[int] | None = None,
|
||||
view: str = "iso",
|
||||
) -> list[ImageContent]:
|
||||
"""Render the loaded IFC model to a PNG image.
|
||||
|
||||
Returns an inline image the LLM can inspect to understand the spatial
|
||||
layout of the model or a specific element in context.
|
||||
|
||||
:param selector: ifcopenshell selector to restrict rendered elements
|
||||
(e.g. ``'IfcWall'``, ``'IfcBuildingStorey[Name="0"]'``).
|
||||
Omit to render the whole model.
|
||||
:param element_ids: Step IDs of elements to highlight. Other elements
|
||||
are rendered in translucent grey so the subject stands out.
|
||||
:param view: Camera angle — ``iso`` (default), ``top``, ``south``,
|
||||
``north``, ``east``, or ``west``.
|
||||
"""
|
||||
png_bytes = session.ifc_render(selector=selector, element_ids=element_ids, view=view)
|
||||
return [ImageContent(type="image", data=base64.b64encode(png_bytes).decode(), mimeType="image/png")]
|
||||
|
||||
return server
|
||||
@@ -21,13 +21,14 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import ifcopenshell
|
||||
|
||||
from ifcquery import clash as clash_mod
|
||||
from ifcquery import cost as cost_mod
|
||||
from ifcquery import info, relations, schedule, schema, select, summary, tree
|
||||
from ifcquery import info, relations, render as render_mod, schedule, schema, select, summary, tree
|
||||
from ifcquery import validate as validate_mod
|
||||
|
||||
|
||||
@@ -123,6 +124,24 @@ def main():
|
||||
schema_parser = subparsers.add_parser("schema", help="IFC class documentation")
|
||||
schema_parser.add_argument("entity_type", help="IFC entity type (e.g. IfcWall)")
|
||||
|
||||
render_parser = subparsers.add_parser("render", help="Render model geometry to a PNG image")
|
||||
render_parser.add_argument(
|
||||
"-o", "--output", default="", metavar="FILE", help="Output PNG path (default: <ifc_file>.png)"
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--selector", default="", metavar="QUERY", help="ifcopenshell selector to restrict rendered elements"
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--element", default="", metavar="ID[,ID...]",
|
||||
help="Comma-separated step IDs of elements to highlight (rest rendered in grey)"
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--view",
|
||||
choices=render_mod.VIEWS,
|
||||
default="iso",
|
||||
help="Camera angle (default: iso)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
@@ -187,6 +206,32 @@ def main():
|
||||
result = cost_mod.cost(model, max_depth=args.depth)
|
||||
elif args.command == "schema":
|
||||
result = schema.schema(model, args.entity_type)
|
||||
elif args.command == "render":
|
||||
element_ids = None
|
||||
if args.element:
|
||||
try:
|
||||
element_ids = [parse_element_id(part) for part in args.element.split(",")]
|
||||
except ValueError:
|
||||
print(f"Error: Invalid element ID(s): {args.element}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
out_path = args.output or (os.path.splitext(args.ifc_file)[0] + ".png")
|
||||
try:
|
||||
png_bytes = render_mod.render(
|
||||
model,
|
||||
selector=args.selector or None,
|
||||
element_ids=element_ids,
|
||||
view=args.view,
|
||||
)
|
||||
except ImportError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(png_bytes)
|
||||
print(f"Saved render to {out_path}", file=sys.stderr)
|
||||
return
|
||||
|
||||
print(format_output(result, args.output_format))
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# IfcQuery - IFC model interrogation CLI
|
||||
# Copyright (C) 2026 Bruno Postle <bruno@postle.net>
|
||||
#
|
||||
# This file is part of IfcQuery.
|
||||
#
|
||||
# IfcQuery is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcQuery is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcQuery. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import multiprocessing
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.selector
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
import pyvista as pv
|
||||
|
||||
_HAS_PYVISTA = True
|
||||
except ImportError:
|
||||
_HAS_PYVISTA = False
|
||||
|
||||
VIEWS = ("iso", "top", "south", "north", "east", "west")
|
||||
|
||||
|
||||
def _apply_view(plotter: "pv.Plotter", view: str) -> None:
|
||||
"""Set the camera to the requested named view. Z is up (IFC convention)."""
|
||||
if view == "top":
|
||||
plotter.view_xy()
|
||||
elif view == "south":
|
||||
# Camera at -Y looking toward +Y (south face of building)
|
||||
plotter.view_xz(negative=True)
|
||||
elif view == "north":
|
||||
plotter.view_xz(negative=False)
|
||||
elif view == "east":
|
||||
plotter.view_yz(negative=False)
|
||||
elif view == "west":
|
||||
plotter.view_yz(negative=True)
|
||||
else:
|
||||
plotter.view_isometric()
|
||||
# Ensure Z is world up for elevation views
|
||||
if view not in ("top",):
|
||||
plotter.camera.up = (0, 0, 1)
|
||||
|
||||
|
||||
def _add_shape(
|
||||
shape: object,
|
||||
plotter: "pv.Plotter",
|
||||
highlight_ids: frozenset[int] | None,
|
||||
) -> None:
|
||||
"""Triangulate and add a geometry shape to the plotter."""
|
||||
geom = shape.geometry
|
||||
verts = np.array(geom.verts, dtype=float).reshape(-1, 3)
|
||||
if verts.size == 0:
|
||||
return
|
||||
|
||||
faces = np.array(geom.faces, dtype=int).reshape(-1, 3)
|
||||
material_ids = np.array(geom.material_ids, dtype=int)
|
||||
|
||||
is_subject = highlight_ids is not None and shape.product.id() in highlight_ids
|
||||
|
||||
for midx, mat in enumerate(geom.materials):
|
||||
tri_mask = material_ids == midx
|
||||
if not np.any(tri_mask):
|
||||
continue
|
||||
|
||||
sub_faces = faces[tri_mask]
|
||||
faces_pv = np.hstack([np.full((sub_faces.shape[0], 1), 3, dtype=int), sub_faces]).ravel()
|
||||
mesh = pv.PolyData(verts, faces_pv)
|
||||
|
||||
if highlight_ids is not None and not is_subject:
|
||||
color = (180, 180, 180)
|
||||
opacity = 0.10
|
||||
else:
|
||||
diffuse = np.clip(np.array(mat.diffuse.components), 0.0, 1.0)
|
||||
color = tuple((diffuse * 255).astype(np.uint8))
|
||||
transparency = mat.transparency if mat.transparency == mat.transparency else 0.0
|
||||
opacity = float(np.clip(1.0 - transparency, 0.0, 1.0))
|
||||
|
||||
plotter.add_mesh(mesh, color=color, opacity=opacity, show_edges=False)
|
||||
|
||||
|
||||
def render(
|
||||
model: ifcopenshell.file,
|
||||
selector: str | None = None,
|
||||
element_ids: list[int] | None = None,
|
||||
view: str = "iso",
|
||||
) -> bytes:
|
||||
"""Render IFC model geometry to a PNG image.
|
||||
|
||||
:param model: The in-memory IFC model.
|
||||
:param selector: ifcopenshell selector to restrict rendered elements
|
||||
(e.g. ``'IfcWall'`` or ``'IfcBuildingStorey[Name="Ground Floor"]'``).
|
||||
When omitted the whole model is rendered.
|
||||
:param element_ids: Step IDs of elements to highlight. The rest of the
|
||||
model is rendered in translucent grey so the highlighted elements
|
||||
stand out.
|
||||
:param view: Camera angle: ``iso``, ``top``, ``south``, ``north``,
|
||||
``east``, or ``west``. Defaults to ``iso``.
|
||||
:return: PNG image as raw bytes.
|
||||
:raises ImportError: If pyvista is not installed.
|
||||
:raises ValueError: If the selector matches nothing or the model has no
|
||||
renderable geometry.
|
||||
"""
|
||||
if not _HAS_PYVISTA:
|
||||
raise ImportError("pyvista is not installed. Install with: pip install pyvista")
|
||||
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("use-world-coords", True)
|
||||
|
||||
# Exclude 'Clearance' subcontexts (door/window operation zones) from rendering.
|
||||
clearance_ids = {
|
||||
c.id()
|
||||
for c in model.by_type("IfcGeometricRepresentationSubContext")
|
||||
if c.ContextIdentifier == "Clearance"
|
||||
}
|
||||
if clearance_ids:
|
||||
ctx_ids = [
|
||||
c.id()
|
||||
for c in model.by_type("IfcGeometricRepresentationContext")
|
||||
if c.id() not in clearance_ids
|
||||
]
|
||||
if ctx_ids:
|
||||
settings.set("context-ids", ctx_ids)
|
||||
|
||||
if selector:
|
||||
include_elements = list(ifcopenshell.util.selector.filter_elements(model, selector))
|
||||
if not include_elements:
|
||||
raise ValueError(f"Selector {selector!r} matched no elements")
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
settings,
|
||||
model,
|
||||
multiprocessing.cpu_count(),
|
||||
include=include_elements,
|
||||
)
|
||||
else:
|
||||
exclude = list(model.by_type("IfcOpeningElement"))
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
settings,
|
||||
model,
|
||||
multiprocessing.cpu_count(),
|
||||
exclude=exclude if exclude else None,
|
||||
)
|
||||
|
||||
if not iterator.initialize():
|
||||
raise ValueError("No renderable geometry found in model (or selector matched nothing)")
|
||||
|
||||
plotter = pv.Plotter(off_screen=True, window_size=(1280, 960))
|
||||
plotter.background_color = "white"
|
||||
|
||||
while True:
|
||||
_add_shape(iterator.get(), plotter, highlight_ids=frozenset(element_ids) if element_ids else None)
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
plotter.reset_camera()
|
||||
_apply_view(plotter, view)
|
||||
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".png")
|
||||
os.close(tmp_fd)
|
||||
try:
|
||||
plotter.show(screenshot=tmp_path, auto_close=True)
|
||||
with open(tmp_path, "rb") as f:
|
||||
return f.read()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
@@ -0,0 +1,221 @@
|
||||
# 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 numpy as np
|
||||
import pytest
|
||||
|
||||
from ifcquery.render import 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
|
||||
|
||||
|
||||
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 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
|
||||
Reference in New Issue
Block a user