# IfcOpenShell - IFC toolkit and geometry engine # Copyright (C) 2021 Thomas Krijnen # # This file is part of IfcOpenShell. # # IfcOpenShell 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. # # IfcOpenShell 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 IfcOpenShell. If not, see . import os import re import glob import json import typing import shutil import subprocess from dataclasses import dataclass, field import pytest import ifcopenshell import ifcopenshell.guid import ifcopenshell.template PERF = False @dataclass class rect: width: float height: float def build(self, f): return f.createIfcRectangleProfileDef("AREA", None, None, self.width, self.height) @dataclass class circle: radius: float def build(self, f): return f.createIfcCircleProfileDef("AREA", None, None, self.radius) @dataclass class opening: x: float z: float shape: typing.Any depth: float direc: tuple = field(default_factory=lambda: (0.0, 0.0, -1.0)) O = 0.0, 0.0, 0.0 X = 1.0, 0.0, 0.0 Y = 0.0, 1.0, 0.0 Z = 0.0, 0.0, 1.0 # Creates an IfcAxis2Placement3D from Location, Axis and RefDirection specified as Python tuples def create_ifcaxis2placement(f, point=O, dir1=Z, dir2=X): point = f.createIfcCartesianPoint(point) dir1 = f.createIfcDirection(dir1) dir2 = f.createIfcDirection(dir2) axis2placement = f.createIfcAxis2Placement3D(point, dir1, dir2) return axis2placement # Creates an IfcLocalPlacement from Location, Axis and RefDirection, specified as Python tuples, and relative placement def create_ifclocalplacement(f, point=O, dir1=Z, dir2=X, relative_to=None): axis2placement = create_ifcaxis2placement(f, point, dir1, dir2) ifclocalplacement2 = f.createIfcLocalPlacement(relative_to, axis2placement) return ifclocalplacement2 # Creates an IfcPolyLine from a list of points, specified as Python tuples def create_ifcpolyline(f, point_list): ifcpts = [] for point in point_list: point = f.createIfcCartesianPoint(point) ifcpts.append(point) polyline = f.createIfcPolyLine(ifcpts) return polyline # Creates an IfcExtrudedAreaSolid from a list of points, specified as Python tuples def create_ifcextrudedareasolid(f, point_list, ifcaxis2placement, extrude_dir, extrusion): polyline = create_ifcpolyline(f, point_list) ifcclosedprofile = f.createIfcArbitraryClosedProfileDef("AREA", None, polyline) ifcdir = f.createIfcDirection(extrude_dir) ifcextrudedareasolid = f.createIfcExtrudedAreaSolid(ifcclosedprofile, ifcaxis2placement, ifcdir, extrusion) return ifcextrudedareasolid def create_case(fn, openings): f = ifcopenshell.template.create() owner_history = f.by_type("IfcOwnerHistory")[0] project = f.by_type("IfcProject")[0] context = f.by_type("IfcGeometricRepresentationContext")[0] wall_placement = create_ifclocalplacement(f, relative_to=None) extrusion_placement = create_ifcaxis2placement(f, (0.0, 0.0, 0.0), (0.0, 0.0, 1.0), (1.0, 0.0, 0.0)) point_list_extrusion_area = [ (0.0, -0.2, 0.0), (15.0, -0.2, 0.0), (15.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, -0.2, 0.0), ] solid = create_ifcextrudedareasolid(f, point_list_extrusion_area, extrusion_placement, (0.0, 0.0, 1.0), 4.0) body_representation = f.createIfcShapeRepresentation(context, "Body", "SweptSolid", [solid]) product_shape = f.createIfcProductDefinitionShape(None, None, [body_representation]) wall = f.createIfcWallStandardCase( ifcopenshell.guid.new(), owner_history, "Wall", None, None, wall_placement, product_shape, None ) for opening in openings: opening_placement = create_ifclocalplacement( f, (opening.x, 0.0, opening.z), (0.0, 1.0, 0.0), (1.0, 0.0, 0.0), wall_placement ) opening_solid = f.createIfcExtrudedAreaSolid( opening.shape.build(f), None, f.createIfcDirection(opening.direc), opening.depth ) opening_representation = f.createIfcShapeRepresentation(context, "Body", "SweptSolid", [opening_solid]) opening_shape = f.createIfcProductDefinitionShape(None, None, [opening_representation]) opening_element = f.createIfcOpeningElement( ifcopenshell.guid.new(), owner_history, "Opening", None, None, opening_placement, opening_shape, None ) f.createIfcRelVoidsElement(ifcopenshell.guid.new(), owner_history, None, None, wall, opening_element) f.write(fn) class TestWallOpenings: @pytest.mark.skipif(shutil.which("IfcConvert") is None, reason="Requires IfcConvert in path") def test_all(self): cases = [ ( "wall-openings-non-intersecting-rect-circle.ifc", [opening(i * 4.0 + 2.0, 2.0, rect(1.0, 1.0), 0.2) for i in range(3)] + [opening(i * 4.0 + 4.0, 2.0, circle(0.5), 0.2) for i in range(3)], ), ( "wall-openings-intersecting-inner-bounds.ifc", [opening(i * 0.8 + 2.0, i * 0.1 + 1.0, rect(1.0, 1.0), 0.2) for i in range(15)], ), ( "wall-openings-intersecting-with-outer.ifc", [opening(i * 2.0, 4.0, rect(1.0, 1.0), 0.2) for i in range(15)], ), ( "wall-openings-recesses.ifc", [opening(i * 4.0 + 2.0, 2.0, rect(1.0, 1.0), 0.1) for i in range(3)] + [opening(i * 4.0 + 4.0, 2.0, circle(0.5), 0.1) for i in range(3)], ), ( "wall-openings-non-orthogonal.ifc", [opening(i * 4.0 + 2.0, 2.0, rect(1.0, 1.0), 0.3, direc=(1.0, 0.0, -1.0)) for i in range(6)], ), ( "wall-openings-contained-in-other.ifc", [opening(2.0, 2.0, rect(1.0, 1.0), 0.2), opening(2.0, 2.0, rect(0.5, 0.5), 0.2)], ), ( "wall-openings-outside-of-outer.ifc", [opening(2.0, 2.0, rect(1.0, 1.0), 0.2), opening(2.0, 6.0, rect(1.0, 1.0), 0.2)], ), ("wall-openings-touching-outer.ifc", [opening(2.0, 3.0, rect(2.0, 2.0), 0.2)]), ] checks = [ [(1, "Processed fully in 2D")], [(1, "Intersecting boundaries")], [(1, "Intersecting boundaries")], [(1, "No second operands can be processed as 2D inner bounds"), (0, "Operand B creates a through hole")], [(0, "Operand B 1/6 is an extrusion")], [(1, "Subtraction operand contained in other"), (0, "Subtraction operand outside of outer bound")], [], # [(1, "Subtraction operand outside of outer bound")], [(1, "Intersecting boundaries")], ] for fn in glob.glob("*.log.json"): os.unlink(fn) pat = re.compile(r"^([\w :]+?)\s*: (\d+\.\d+)") result = [] for ci, ((fn, ops), cs) in enumerate(zip(cases, checks), start=1): create_case(fn, ops) result.append([fn]) for i in range(2 if PERF else 1): args = [ shutil.which("IfcConvert") or "IfcConvert", "-qyvvv", fn, fn + ".obj", "--log-format", "json", "--log-file", fn + ".log.json", ] if i: args.append("--no-2d-boolean") ts = [] for j in range(10 if PERF else 1): subprocess.call(args, stdout=subprocess.PIPE) log = [json.loads(ln)["message"] for ln in open(fn + ".log.json") if ln] perf = dict(x.groups() for x in [re.match(pat, l) for l in log] if x) ts.append(float(perf["file geometry conversion"])) result[-1].append(sum(ts) / len(ts)) if i == 0 and j == 0: for ln, st in cs: assert len([l for l in log if l.startswith(st)]) == ln, ( f"\nOn file:\n - {fn}\nMessages:" + "".join(f'\n - "{l}"' for l in log) + f'\nExpected:\n - "{st}"' ) # breakpoint() temp_files = [fn, f"{fn}.obj", f"{fn}.log.json", f"{fn}.mtl"] for temp_fn in temp_files: os.unlink(temp_fn) try: import tabulate except: return print("\n" + tabulate.tabulate(result, headers=["file", "", "--no-2d-boolean"], tablefmt="github")) if __name__ == "__main__": pytest.main(["-x", __file__])