mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
Add ifcquery clash subcommand for geometric clash detection
Uses ifcopenshell.geom.tree API directly to check an element for intersections and clearance violations against sibling elements (--scope storey) or the entire model (--scope all).
This commit is contained in:
@@ -24,6 +24,7 @@ import sys
|
||||
|
||||
import ifcopenshell
|
||||
|
||||
from ifcquery import clash as clash_mod
|
||||
from ifcquery import info, relations, select, summary, tree
|
||||
|
||||
|
||||
@@ -93,6 +94,14 @@ def main():
|
||||
relations_parser.add_argument("element_id", help="Element step ID (e.g. 123 or #123)")
|
||||
relations_parser.add_argument("--traverse", choices=["up"], help="Traverse hierarchy (up: walk to IfcProject)")
|
||||
|
||||
clash_parser = subparsers.add_parser("clash", help="Check element placement for clashes")
|
||||
clash_parser.add_argument("element_id", help="Element step ID (e.g. 123 or #123)")
|
||||
clash_parser.add_argument("--clearance", type=float, help="Minimum clearance distance")
|
||||
clash_parser.add_argument("--tolerance", type=float, default=0.002, help="Intersection tolerance (default: 0.002)")
|
||||
clash_parser.add_argument(
|
||||
"--scope", choices=["storey", "all"], default="storey", help="Scope of elements to check (default: storey)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
@@ -131,6 +140,24 @@ def main():
|
||||
print(f"Error: Element #{element_id} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
result = relations.relations(model, element, traverse=args.traverse)
|
||||
elif args.command == "clash":
|
||||
try:
|
||||
element_id = parse_element_id(args.element_id)
|
||||
except ValueError:
|
||||
print(f"Error: Invalid element ID: {args.element_id}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
try:
|
||||
element = model.by_id(element_id)
|
||||
except RuntimeError:
|
||||
print(f"Error: Element #{element_id} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
try:
|
||||
result = clash_mod.clash(
|
||||
model, element, clearance=args.clearance, tolerance=args.tolerance, scope=args.scope
|
||||
)
|
||||
except ImportError:
|
||||
print("Error: ifcopenshell geometry engine not available (C++ bindings required)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(format_output(result, args.output_format))
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# IfcQuery - IFC model interrogation CLI
|
||||
# Copyright (C) 2025 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 sys
|
||||
from typing import Any
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def _ref(element: ifcopenshell.entity_instance) -> dict[str, Any]:
|
||||
"""Serialize an element to a compact reference dict."""
|
||||
result: dict[str, Any] = {"id": element.id(), "type": element.is_a()}
|
||||
if hasattr(element, "Name") and element.Name:
|
||||
result["name"] = element.Name
|
||||
return result
|
||||
|
||||
|
||||
def _get_scope_elements(
|
||||
model: ifcopenshell.file, element: ifcopenshell.entity_instance, scope: str
|
||||
) -> tuple[set[ifcopenshell.entity_instance], str]:
|
||||
"""Return set of elements to check against and the effective scope used.
|
||||
|
||||
Returns (elements, effective_scope) where effective_scope may differ from
|
||||
the requested scope if fallback was needed.
|
||||
"""
|
||||
if scope == "storey":
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container is not None:
|
||||
siblings = set(ifcopenshell.util.element.get_contained(container))
|
||||
siblings.discard(element)
|
||||
return siblings, "storey"
|
||||
else:
|
||||
print(
|
||||
f"Warning: Element #{element.id()} has no spatial container, falling back to --scope all",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# scope == "all" or fallback
|
||||
elements = set(model.by_type("IfcElement"))
|
||||
elements -= set(model.by_type("IfcFeatureElement"))
|
||||
elements.discard(element)
|
||||
return elements, "all"
|
||||
|
||||
|
||||
def _build_tree(
|
||||
model: ifcopenshell.file, elements: set[ifcopenshell.entity_instance]
|
||||
) -> ifcopenshell.geom.tree | None:
|
||||
"""Build geometry tree for given elements using iterator.
|
||||
|
||||
Returns None if iterator fails to initialize (no geometry available).
|
||||
"""
|
||||
geom_settings = ifcopenshell.geom.settings()
|
||||
geom_settings.set("use-world-coords", True)
|
||||
geom_tree = ifcopenshell.geom.tree()
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
geom_settings, model, multiprocessing.cpu_count(), include=list(elements)
|
||||
)
|
||||
if not iterator.initialize():
|
||||
return None
|
||||
while True:
|
||||
geom_tree.add_element(iterator.get())
|
||||
if not iterator.next():
|
||||
break
|
||||
return geom_tree
|
||||
|
||||
|
||||
def _format_clash(
|
||||
clash_result, geom_tree: ifcopenshell.geom.tree, model: ifcopenshell.file
|
||||
) -> dict[str, Any]:
|
||||
"""Format a single clash result to dict."""
|
||||
# clash result .a/.b are C++ wrapper entity_instances without .Name;
|
||||
# look up the Python entity from the model by id for proper serialization
|
||||
other = model.by_id(clash_result.b.id())
|
||||
return {
|
||||
"element": _ref(other),
|
||||
"type": geom_tree.get_clash_type(clash_result.clash_type),
|
||||
"distance": clash_result.distance,
|
||||
"p1": list(clash_result.p1),
|
||||
"p2": list(clash_result.p2),
|
||||
}
|
||||
|
||||
|
||||
def clash(
|
||||
model: ifcopenshell.file,
|
||||
element: ifcopenshell.entity_instance,
|
||||
clearance: float | None = None,
|
||||
tolerance: float = 0.002,
|
||||
scope: str = "storey",
|
||||
) -> dict[str, Any]:
|
||||
"""Check element for geometric clashes against other elements.
|
||||
|
||||
:param model: The IFC model.
|
||||
:param element: The element to check.
|
||||
:param clearance: Minimum clearance distance; if provided, runs clearance check.
|
||||
:param tolerance: Intersection tolerance in meters (default 0.002).
|
||||
:param scope: Which elements to check against: "storey" or "all".
|
||||
:return: Dict with clash results suitable for JSON serialization.
|
||||
"""
|
||||
result: dict[str, Any] = {"element": _ref(element)}
|
||||
|
||||
# Get scope elements
|
||||
scope_elements, effective_scope = _get_scope_elements(model, element, scope)
|
||||
result["scope"] = effective_scope
|
||||
|
||||
if not scope_elements:
|
||||
result["pass"] = True
|
||||
result["checks"] = {
|
||||
"intersection": {"pass": True, "tolerance": tolerance, "clashes": []}
|
||||
}
|
||||
if clearance is not None:
|
||||
result["checks"]["clearance"] = {"pass": True, "clearance": clearance, "clashes": []}
|
||||
return result
|
||||
|
||||
# Build geometry tree for target element + scope elements
|
||||
all_elements = scope_elements | {element}
|
||||
geom_tree = _build_tree(model, all_elements)
|
||||
|
||||
if geom_tree is None:
|
||||
result["pass"] = None
|
||||
result["error"] = f"No geometry for element #{element.id()}"
|
||||
return result
|
||||
|
||||
# Run intersection check
|
||||
intersection_clashes = geom_tree.clash_intersection_many(
|
||||
[element], list(scope_elements), tolerance=tolerance, check_all=True
|
||||
)
|
||||
intersection_results = [_format_clash(c, geom_tree, model) for c in intersection_clashes]
|
||||
checks: dict[str, Any] = {
|
||||
"intersection": {
|
||||
"pass": len(intersection_results) == 0,
|
||||
"tolerance": tolerance,
|
||||
"clashes": intersection_results,
|
||||
}
|
||||
}
|
||||
|
||||
all_pass = len(intersection_results) == 0
|
||||
|
||||
# Run clearance check if requested
|
||||
if clearance is not None:
|
||||
clearance_clashes = geom_tree.clash_clearance_many(
|
||||
[element], list(scope_elements), clearance=clearance, check_all=True
|
||||
)
|
||||
clearance_results = [_format_clash(c, geom_tree, model) for c in clearance_clashes]
|
||||
checks["clearance"] = {
|
||||
"pass": len(clearance_results) == 0,
|
||||
"clearance": clearance,
|
||||
"clashes": clearance_results,
|
||||
}
|
||||
if clearance_results:
|
||||
all_pass = False
|
||||
|
||||
result["pass"] = all_pass
|
||||
result["checks"] = checks
|
||||
return result
|
||||
@@ -0,0 +1,332 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
from ifcquery.clash import clash
|
||||
|
||||
try:
|
||||
import ifcopenshell.geom
|
||||
|
||||
HAS_GEOM = True
|
||||
except ImportError:
|
||||
HAS_GEOM = False
|
||||
|
||||
pytestmark = pytest.mark.skipif(not HAS_GEOM, reason="ifcopenshell geometry engine not available")
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
# Create geometry context
|
||||
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
|
||||
)
|
||||
|
||||
# Wall 1 at origin
|
||||
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)
|
||||
|
||||
# Wall 2 perpendicular, crossing through wall 1
|
||||
wall2 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall002")
|
||||
rep2 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, 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.array([[0, -1, 0, 2.5], [1, 0, 0, -2.0], [0, 0, 1, 0], [0, 0, 0, 1]], dtype=float)
|
||||
ifcopenshell.api.geometry.edit_object_placement(f, product=wall2, matrix=matrix2)
|
||||
|
||||
# Wall 3 far away (10m offset in Y)
|
||||
wall3 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall003")
|
||||
rep3 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
|
||||
ifcopenshell.api.geometry.assign_representation(f, product=wall3, representation=rep3)
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[wall3], relating_structure=storey)
|
||||
matrix3 = np.eye(4)
|
||||
matrix3[1, 3] = 10.0 # 10m in Y direction
|
||||
ifcopenshell.api.geometry.edit_object_placement(f, product=wall3, matrix=matrix3)
|
||||
|
||||
# Wall 4 close but not overlapping (0.3m offset in Y, wall thickness is 0.2m)
|
||||
wall4 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall004")
|
||||
rep4 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
|
||||
ifcopenshell.api.geometry.assign_representation(f, product=wall4, representation=rep4)
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[wall4], relating_structure=storey)
|
||||
matrix4 = np.eye(4)
|
||||
matrix4[1, 3] = 0.3 # 0.3m in Y (gap of 0.1m from wall1)
|
||||
ifcopenshell.api.geometry.edit_object_placement(f, product=wall4, matrix=matrix4)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model_two_storeys():
|
||||
"""Create a model with walls in different storeys."""
|
||||
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")
|
||||
storey1 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
|
||||
storey2 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="First 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=[storey1, storey2], 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
|
||||
)
|
||||
|
||||
# Wall in storey 1
|
||||
wall1 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="GroundWall")
|
||||
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=storey1)
|
||||
|
||||
# Wall in storey 2, perpendicular and crossing wall1
|
||||
wall2 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="FirstFloorWall")
|
||||
rep2 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, 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=storey2)
|
||||
matrix2 = np.array([[0, -1, 0, 2.5], [1, 0, 0, -2.0], [0, 0, 1, 0], [0, 0, 0, 1]], dtype=float)
|
||||
ifcopenshell.api.geometry.edit_object_placement(f, product=wall2, matrix=matrix2)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
class TestNoClashes:
|
||||
def test_no_clashes_far_apart(self, model_with_geometry):
|
||||
wall3 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall003"][0]
|
||||
result = clash(model_with_geometry, wall3)
|
||||
assert result["pass"] is True
|
||||
assert result["checks"]["intersection"]["pass"] is True
|
||||
assert result["checks"]["intersection"]["clashes"] == []
|
||||
|
||||
def test_no_clashes_empty_scope(self, model_with_geometry):
|
||||
"""A model where the element is the only one in scope should pass."""
|
||||
# Create a model with a single wall
|
||||
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)
|
||||
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
|
||||
)
|
||||
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="OnlyWall")
|
||||
rep = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
|
||||
ifcopenshell.api.geometry.assign_representation(f, product=wall, representation=rep)
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=storey)
|
||||
|
||||
result = clash(f, wall)
|
||||
assert result["pass"] is True
|
||||
|
||||
|
||||
class TestIntersectionDetected:
|
||||
def test_overlapping_walls(self, model_with_geometry):
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = clash(model_with_geometry, wall1)
|
||||
assert result["pass"] is False
|
||||
assert result["checks"]["intersection"]["pass"] is False
|
||||
clashes = result["checks"]["intersection"]["clashes"]
|
||||
assert len(clashes) > 0
|
||||
# Wall002 should be in the clashes (it overlaps wall1)
|
||||
clash_ids = {c["element"]["id"] for c in clashes}
|
||||
wall2 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall002"][0]
|
||||
assert wall2.id() in clash_ids
|
||||
|
||||
def test_clash_has_points(self, model_with_geometry):
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = clash(model_with_geometry, wall1)
|
||||
clashes = result["checks"]["intersection"]["clashes"]
|
||||
for c in clashes:
|
||||
assert "p1" in c
|
||||
assert "p2" in c
|
||||
assert len(c["p1"]) == 3
|
||||
assert len(c["p2"]) == 3
|
||||
assert "type" in c
|
||||
assert "distance" in c
|
||||
|
||||
|
||||
class TestClearance:
|
||||
def test_clearance_violation(self, model_with_geometry):
|
||||
"""Wall004 is 0.1m from wall1; clearance of 0.5m should fail."""
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = clash(model_with_geometry, wall1, clearance=0.5)
|
||||
assert "clearance" in result["checks"]
|
||||
# Wall004 should violate clearance
|
||||
clearance_clashes = result["checks"]["clearance"]["clashes"]
|
||||
clash_ids = {c["element"]["id"] for c in clearance_clashes}
|
||||
wall4 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall004"][0]
|
||||
assert wall4.id() in clash_ids
|
||||
assert result["checks"]["clearance"]["pass"] is False
|
||||
|
||||
def test_clearance_pass(self, model_with_geometry):
|
||||
"""Wall003 is 10m away; clearance of 0.5m should pass for wall003."""
|
||||
wall3 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall003"][0]
|
||||
result = clash(model_with_geometry, wall3, clearance=0.5)
|
||||
assert result["checks"]["clearance"]["pass"] is True
|
||||
assert result["checks"]["clearance"]["clashes"] == []
|
||||
|
||||
def test_clearance_not_included_by_default(self, model_with_geometry):
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = clash(model_with_geometry, wall1)
|
||||
assert "clearance" not in result["checks"]
|
||||
|
||||
|
||||
class TestScope:
|
||||
def test_scope_storey_excludes_other_storeys(self, model_two_storeys):
|
||||
wall1 = [w for w in model_two_storeys.by_type("IfcWall") if w.Name == "GroundWall"][0]
|
||||
result = clash(model_two_storeys, wall1, scope="storey")
|
||||
assert result["scope"] == "storey"
|
||||
# No clashes because the overlapping wall is in a different storey
|
||||
assert result["pass"] is True
|
||||
|
||||
def test_scope_all_includes_other_storeys(self, model_two_storeys):
|
||||
wall1 = [w for w in model_two_storeys.by_type("IfcWall") if w.Name == "GroundWall"][0]
|
||||
result = clash(model_two_storeys, wall1, scope="all")
|
||||
assert result["scope"] == "all"
|
||||
# Should detect clash with the other-storey wall
|
||||
assert result["pass"] is False
|
||||
clash_ids = {c["element"]["id"] for c in result["checks"]["intersection"]["clashes"]}
|
||||
wall2 = [w for w in model_two_storeys.by_type("IfcWall") if w.Name == "FirstFloorWall"][0]
|
||||
assert wall2.id() in clash_ids
|
||||
|
||||
|
||||
class TestNoGeometry:
|
||||
def test_no_geometry_error(self, model):
|
||||
"""Element without geometry reports error."""
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = clash(model, wall)
|
||||
assert result["pass"] is None
|
||||
assert "error" in result
|
||||
assert "No geometry" in result["error"]
|
||||
|
||||
|
||||
class TestJsonSerializable:
|
||||
def test_result_serializable(self, model_with_geometry):
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = clash(model_with_geometry, wall1)
|
||||
serialized = json.dumps(result)
|
||||
parsed = json.loads(serialized)
|
||||
assert parsed["element"]["type"] == "IfcWall"
|
||||
|
||||
def test_clearance_result_serializable(self, model_with_geometry):
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = clash(model_with_geometry, wall1, clearance=0.5)
|
||||
serialized = json.dumps(result)
|
||||
parsed = json.loads(serialized)
|
||||
assert "clearance" in parsed["checks"]
|
||||
|
||||
|
||||
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_clash_json(self, model_with_geometry):
|
||||
path = self._ifc_path(model_with_geometry)
|
||||
try:
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "ifcquery", path, "clash", str(wall1.id())],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
data = json.loads(result.stdout)
|
||||
assert data["element"]["type"] == "IfcWall"
|
||||
assert "checks" in data
|
||||
assert "intersection" in data["checks"]
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_clash_with_clearance(self, model_with_geometry):
|
||||
path = self._ifc_path(model_with_geometry)
|
||||
try:
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "ifcquery", path, "clash", str(wall1.id()), "--clearance", "0.5"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
data = json.loads(result.stdout)
|
||||
assert "clearance" in data["checks"]
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_clash_scope_all(self, model_with_geometry):
|
||||
path = self._ifc_path(model_with_geometry)
|
||||
try:
|
||||
wall1 = [w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001"][0]
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "ifcquery", path, "clash", str(wall1.id()), "--scope", "all"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
data = json.loads(result.stdout)
|
||||
assert data["scope"] == "all"
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_clash_bad_id(self, model_with_geometry):
|
||||
path = self._ifc_path(model_with_geometry)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "ifcquery", path, "clash", "999999"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode != 0
|
||||
assert "Error" in result.stderr
|
||||
finally:
|
||||
os.unlink(path)
|
||||
Reference in New Issue
Block a user