IfcPatch now allows passing in IFC objects instead of filepath strings

This commit is contained in:
Dion Moult
2022-09-19 13:31:33 +10:00
parent ea1d797605
commit 5c5bb03652
21 changed files with 104 additions and 123 deletions
+1 -3
View File
@@ -57,10 +57,8 @@ Here is a minimal example of how to use IfcDiff as a library:
import ifcpatch
ifcpatch.execute({
"input": "input.ifc",
"output": "output.ifc",
"input": ifcopenshell.open("input.ifc"),
"recipe": "ExtractElements",
"log": "ifcpatch.log",
"arguments": [".IfcWall"],
})
+11 -28
View File
@@ -18,8 +18,6 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
# This can be packaged into one executable with ./make.py
import ifcopenshell
import logging
import os
@@ -29,39 +27,24 @@ import collections
import importlib
def execute(args, is_library=None):
logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG)
def execute(args):
if "log" in args:
logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG)
logger = logging.getLogger("IFCPatch")
print("# Loading IFC file ...")
ifc_file = ifcopenshell.open(args["input"])
print("# Loading patch recipe ...")
ifc_file = args["input"]
recipes = getattr(__import__("ifcpatch.recipes.{}".format(args["recipe"])), "recipes")
recipe = getattr(recipes, args["recipe"])
if recipe.Patcher.__init__.__doc__ is not None:
patcher = recipe.Patcher(args["input"], ifc_file, logger, *args["arguments"])
else:
patcher = recipe.Patcher(args["input"], ifc_file, logger, args["arguments"])
print("# Patching ...")
patcher.patch()
ifc_file = getattr(patcher, "file_patched", patcher.file)
if is_library is True:
return ifc_file
print("# Writing patched file ...")
if not args["output"]:
args["output"] = args["input"]
if isinstance(ifc_file, str):
with open(args["output"], "w") as text_file:
text_file.write(ifc_file)
else:
ifc_file.write(args["output"])
print("# All tasks are complete :-)")
return getattr(patcher, "file_patched", patcher.file)
def extract_docs(
submodule_name: str,
cls_name: str,
method_name: str="__init__",
boilerplate_args : typing.Iterable[str]=None):
submodule_name: str, cls_name: str, method_name: str = "__init__", boilerplate_args: typing.Iterable[str] = None
):
"""Extract class docstrings and method arguments
:param submodule_name: Submodule from which to extract the class
@@ -70,8 +53,8 @@ def extract_docs(
:param boilerplate_args: String iterable containing arguments that shall not be parsed
"""
spec = importlib.util.spec_from_file_location(
submodule_name,
f"{os.path.dirname(inspect.getabsfile(inspect.currentframe()))}/recipes/{submodule_name}.py")
submodule_name, f"{os.path.dirname(inspect.getabsfile(inspect.currentframe()))}/recipes/{submodule_name}.py"
)
submodule = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(submodule)
@@ -101,7 +84,7 @@ def _extract_docs(cls, method_name, boilerplate_args):
type_hints = typing.get_type_hints(method)
for input_name in inputs.keys():
type_hint = type_hints.get(input_name, None)
if type_hint is None: # The argument is not type-hinted. (Or hinted to None ??)
if type_hint is None: # The argument is not type-hinted. (Or hinted to None ??)
continue
if isinstance(type_hint, typing._UnionGenericAlias):
inputs[input_name]["type"] = [t.__name__ for t in typing.get_args(type_hint)]
@@ -125,7 +108,7 @@ def _extract_docs(cls, method_name, boilerplate_args):
description += line
elif i > 2:
description += "\n" + line
docs["description"] = description.strip()
docs["inputs"] = inputs
return docs
+19 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcPatch.
#
@@ -20,6 +20,7 @@
import argparse
import ifcpatch
import ifcopenshell
parser = argparse.ArgumentParser(description="Patches IFC files to fix badly formatted data")
parser.add_argument("-i", "--input", type=str, required=True, help="The IFC file to patch")
@@ -28,4 +29,20 @@ parser.add_argument("-r", "--recipe", type=str, required=True, help="Name of the
parser.add_argument("-l", "--log", type=str, help="Specify a log file", default="ifcpatch.log")
parser.add_argument("-a", "--arguments", nargs="+", help="Specify custom arguments to the patch recipe")
args = vars(parser.parse_args())
ifcpatch.execute(args)
print("# Loading IFC file ...")
args["input"] = ifcopenshell.open(args["input"])
print("# Patching ...")
ifc_file = ifcpatch.execute(args)
print("# Writing patched file ...")
if not args["output"]:
args["output"] = args["input"]
if isinstance(ifc_file, str):
with open(args["output"], "w") as text_file:
text_file.write(ifc_file)
else:
ifc_file.write(args["output"])
print("# All tasks are complete :-)")
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -37,13 +36,13 @@ class Patcher:
if self.file.schema == "IFC2X3":
user = self.file_patched.add(self.file.by_type("IfcProject")[0].OwnerHistory.OwningUser)
old_get_user = ifcopenshell.api.owner.settings.get_user
ifcopenshell.api.owner.settings.get_user = lambda ifc : user
ifcopenshell.api.owner.settings.get_user = lambda ifc: user
project = ifcopenshell.api.run("root.create_entity", self.file_patched, ifc_class="IfcProject")
unit_assignment = ifcopenshell.api.run("unit.assign_unit", self.file_patched, **{"length": unit})
# Is there a better way?
for element in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
element.Precision = 1E-8
element.Precision = 1e-8
# If we don't add openings first, they don't get converted
for element in self.file.by_type("IfcOpeningElement"):
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -44,13 +44,13 @@ class Patcher:
bm = bmesh.new()
bm.from_mesh(data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.01)
bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method='BEAUTY', ngon_method='BEAUTY')
bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method="BEAUTY", ngon_method="BEAUTY")
bm.faces.ensure_lookup_table()
for polygon in bm.faces:
v1, v2, v3 = [v.co.to_2d() for v in polygon.verts]
d1 = degrees((v2-v1).angle(v3-v1))
d2 = degrees((v3-v2).angle(v1-v2))
d3 = degrees((v1-v3).angle(v2-v3))
d1 = degrees((v2 - v1).angle(v3 - v1))
d2 = degrees((v3 - v2).angle(v1 - v2))
d3 = degrees((v1 - v3).angle(v2 - v3))
if d1 < angle_threshold or d2 < angle_threshold or d3 < angle_threshold:
bm.faces.remove(polygon)
bm.to_mesh(data)
@@ -45,7 +45,7 @@ class Patcher:
#
# See bug https://github.com/Autodesk/revit-ifc/issues/187
#for context in self.file.by_type("IfcGeometricRepresentationSubContext"):
# for context in self.file.by_type("IfcGeometricRepresentationSubContext"):
# if context.ContextIdentifier == "FootPrint":
# context.ContextIdentifier = None
# context.ContextType = "Annotation"
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -20,6 +19,7 @@
import ifcopenshell
import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -29,9 +29,9 @@ class Patcher:
def patch(self):
source = ifcopenshell.open(self.args[0])
original_project = self.file.by_type('IfcProject')[0]
merged_project = self.file.add(source.by_type('IfcProject')[0])
for element in source.by_type('IfcRoot'):
original_project = self.file.by_type("IfcProject")[0]
merged_project = self.file.add(source.by_type("IfcProject")[0])
for element in source.by_type("IfcRoot"):
self.file.add(element)
for inverse in self.file.get_inverse(merged_project):
ifcopenshell.util.element.replace_attribute(inverse, merged_project, original_project)
-1
View File
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -17,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -25,11 +25,11 @@ class Patcher:
self.args = args
def patch(self):
project = self.file.by_type('IfcProject')[0]
storeys = self.find_decomposed_ifc_class(project, 'IfcBuildingStorey')
project = self.file.by_type("IfcProject")[0]
storeys = self.find_decomposed_ifc_class(project, "IfcBuildingStorey")
for storey in storeys:
co = storey.ObjectPlacement.RelativePlacement.Location.Coordinates
storey.ObjectPlacement.RelativePlacement.Location.Coordinates = (co[0], co[1], co[2]+float(self.args[0]))
storey.ObjectPlacement.RelativePlacement.Location.Coordinates = (co[0], co[1], co[2] + float(self.args[0]))
co = storey.ObjectPlacement.RelativePlacement.Location.Coordinates
# NOTE If the geometric data is provided (ObjectPlacement is
# specified), the Elevation value shall either not be included, or
+7 -16
View File
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -22,7 +21,6 @@ import ifcopenshell.util.element
from toposort import toposort_flatten as toposort
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -31,24 +29,22 @@ class Patcher:
self.args = args
self.optimized_file = ifcopenshell.file(schema=self.file.schema)
def patch(self):
def generate_instances_and_references():
"""
Generator which yields an entity id and
the set of all of its references contained in its attributes.
Generator which yields an entity id and
the set of all of its references contained in its attributes.
"""
for inst in self.file:
yield inst.id(), set(i.id() for i in self.file.traverse(inst)[1:] if i.id())
instance_mapping = {}
def map_value(v):
"""
Recursive function which replicates an entity instance, with
Recursive function which replicates an entity instance, with
its attributes, mapping references to already registered
instances. Indeed, because of the toposort we know that
instances. Indeed, because of the toposort we know that
forward attribute value instances are mapped before the instances
that reference them.
"""
@@ -59,15 +55,14 @@ class Patcher:
if v.id() == 0:
# express simple types are not part of the toposort and just copied
return self.optimized_file.create_entity(v.is_a(), v[0])
return instance_mapping[v]
else:
# a plain python value can just be returned
return v
info_to_id = {}
for id in toposort(dict(generate_instances_and_references())):
inst = self.file[id]
info = inst.get_info(include_identifier=False, recursive=True, return_type=frozenset)
@@ -76,10 +71,6 @@ class Patcher:
else:
info_to_id[info] = id
instance_mapping[inst] = self.optimized_file.create_entity(
inst.is_a(),
*map(map_value, inst)
)
instance_mapping[inst] = self.optimized_file.create_entity(inst.is_a(), *map(map_value, inst))
self.file = self.optimized_file
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -20,6 +19,7 @@
from collections import deque
import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -31,7 +31,7 @@ class Patcher:
deleted = []
hashes = {}
for element in self.file:
if element.is_a('IfcRoot'):
if element.is_a("IfcRoot"):
continue
h = hash(tuple(element))
if h in hashes:
@@ -42,13 +42,13 @@ class Patcher:
hashes[h] = element
deleted.sort()
deleted_q = deque(deleted)
new = ''
for line in self.file.wrapped_data.to_string().split('\n'):
new = ""
for line in self.file.wrapped_data.to_string().split("\n"):
try:
if int(line.split('=')[0][1:]) != deleted_q[0]:
new += (line + '\n')
if int(line.split("=")[0][1:]) != deleted_q[0]:
new += line + "\n"
else:
deleted_q.popleft()
except:
new += (line + '\n')
new += line + "\n"
self.file = new
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -17,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -25,8 +25,8 @@ class Patcher:
self.args = args
def patch(self):
project = self.file.by_type('IfcProject')[0]
sites = self.find_decomposed_ifc_class(project, 'IfcSite')
project = self.file.by_type("IfcProject")[0]
sites = self.find_decomposed_ifc_class(project, "IfcSite")
for site in sites:
site.Representation = None
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -17,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -26,8 +26,8 @@ class Patcher:
def patch(self):
placement_coord_ids = set()
for placement in self.file.by_type('IfcObjectPlacement'):
[placement_coord_ids.add(e.id()) for e in self.file.traverse(placement) if e.is_a('IfcCartesianPoint')]
for placement in self.file.by_type("IfcObjectPlacement"):
[placement_coord_ids.add(e.id()) for e in self.file.traverse(placement) if e.is_a("IfcCartesianPoint")]
# Arbitrary threshold based on experience
self.threshold = 1000000
@@ -42,9 +42,9 @@ class Patcher:
# the case, but is very fast to run, and works for most cases.
offset_point = None
if self.args and len(self.args) >= 3:
offset_point = (float(self.args[0]),float(self.args[1]),float(self.args[2]))
offset_point = (float(self.args[0]), float(self.args[1]), float(self.args[2]))
try:
point_lists = self.file.by_type('IfcCartesianPointList3D')
point_lists = self.file.by_type("IfcCartesianPointList3D")
except:
# IFC2X3 does not have IfcCartesianPointList3D
point_lists = []
@@ -56,33 +56,29 @@ class Patcher:
continue
if not offset_point:
offset_point = (-point[0], -point[1], -point[2])
self.logger.info(f'Resetting absolute coordinates by {point}')
point = (
point[0] + offset_point[0],
point[1] + offset_point[1],
point[2] + offset_point[2]
)
self.logger.info(f"Resetting absolute coordinates by {point}")
point = (point[0] + offset_point[0], point[1] + offset_point[1], point[2] + offset_point[2])
coord_list[i] = point
point_list.CoordList = coord_list
for point in self.file.by_type('IfcCartesianPoint'):
for point in self.file.by_type("IfcCartesianPoint"):
if len(point.Coordinates) == 2 or not self.is_point_far_away(point):
continue
if point.id() in placement_coord_ids:
continue
if not offset_point:
offset_point = (-point.Coordinates[0], -point.Coordinates[1], -point.Coordinates[2])
self.logger.info(f'Resetting absolute coordinates by {point}')
self.logger.info(f"Resetting absolute coordinates by {point}")
point.Coordinates = (
point.Coordinates[0] + offset_point[0],
point.Coordinates[1] + offset_point[1],
point.Coordinates[2] + offset_point[2]
point.Coordinates[2] + offset_point[2],
)
def is_point_far_away(self, point):
if hasattr(point, 'Coordinates'):
return abs(point.Coordinates[0]) > self.threshold \
or abs(point.Coordinates[1]) > self.threshold \
if hasattr(point, "Coordinates"):
return (
abs(point.Coordinates[0]) > self.threshold
or abs(point.Coordinates[1]) > self.threshold
or abs(point.Coordinates[2]) > self.threshold
return abs(point[0]) > self.threshold \
or abs(point[1]) > self.threshold \
or abs(point[2]) > self.threshold
)
return abs(point[0]) > self.threshold or abs(point[1]) > self.threshold or abs(point[2]) > self.threshold
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -17,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -25,7 +25,7 @@ class Patcher:
self.args = args
def patch(self):
project = self.file.by_type('IfcProject')[0]
project = self.file.by_type("IfcProject")[0]
spatial_elements = self.find_decomposed_ifc_class(project, self.args[0])
for spatial_element in spatial_elements:
self.patch_placement_to_origin(spatial_element)
@@ -43,8 +43,8 @@ class Patcher:
return results
def patch_placement_to_origin(self, element):
element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0., 0., 0.)
element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0.0, 0.0, 0.0)
if element.ObjectPlacement.RelativePlacement.Axis:
element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0., 0., 1.)
element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0.0, 0.0, 1.0)
if element.ObjectPlacement.RelativePlacement.RefDirection:
element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1., 0., 0.)
element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1.0, 0.0, 0.0)
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -17,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -25,8 +25,8 @@ class Patcher:
self.args = args
def patch(self):
project = self.file.by_type('IfcProject')[0]
sites = self.find_decomposed_ifc_class(project, 'IfcSite')
project = self.file.by_type("IfcProject")[0]
sites = self.find_decomposed_ifc_class(project, "IfcSite")
for site in sites:
site.RefElevation = float(self.args[0])
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -17,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
class Patcher:
def __init__(self, src, file, logger, args=None):
self.src = src
@@ -27,35 +27,39 @@ class Patcher:
def patch(self):
import ifcopenshell
from shutil import copyfile
storeys = self.file.by_type('IfcBuildingStorey')
storeys = self.file.by_type("IfcBuildingStorey")
for i, storey in enumerate(storeys):
dest = '{}-{}.ifc'.format(i, storey.Name)
dest = "{}-{}.ifc".format(i, storey.Name)
copyfile(self.src, dest)
old_ifc = ifcopenshell.open(dest)
new_ifc = ifcopenshell.file(schema=self.file.schema)
if self.file.schema == 'IFC2X3':
elements = old_ifc.by_type('IfcProject') + old_ifc.by_type('IfcProduct')
if self.file.schema == "IFC2X3":
elements = old_ifc.by_type("IfcProject") + old_ifc.by_type("IfcProduct")
else:
elements = old_ifc.by_type('IfcContext') + old_ifc.by_type('IfcProduct')
elements = old_ifc.by_type("IfcContext") + old_ifc.by_type("IfcProduct")
inverse_elements = []
for element in elements:
if element.is_a('IfcElement') \
and not self.is_in_storey(element, storey):
if element.is_a("IfcElement") and not self.is_in_storey(element, storey):
element.Representation = None
continue
if element.is_a('IfcElement'):
styled_rep_items = [i for i in old_ifc.traverse(element) if i.is_a('IfcRepresentationItem') and i.StyledByItem]
if element.is_a("IfcElement"):
styled_rep_items = [
i for i in old_ifc.traverse(element) if i.is_a("IfcRepresentationItem") and i.StyledByItem
]
[new_ifc.add(i.StyledByItem[0]) for i in styled_rep_items]
new_ifc.add(element)
inverse_elements.extend(old_ifc.get_inverse(element))
for inverse_element in inverse_elements:
new_ifc.add(inverse_element)
for element in new_ifc.by_type('IfcElement'):
for element in new_ifc.by_type("IfcElement"):
if not self.is_in_storey(element, storey):
new_ifc.remove(element)
new_ifc.write(dest)
def is_in_storey(self, element, storey):
return element.ContainedInStructure \
and element.ContainedInStructure[0].RelatingStructure.is_a('IfcBuildingStorey') \
return (
element.ContainedInStructure
and element.ContainedInStructure[0].RelatingStructure.is_a("IfcBuildingStorey")
and element.ContainedInStructure[0].RelatingStructure.GlobalId == storey.GlobalId
)
@@ -1,4 +1,3 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
@@ -16,4 +15,3 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
-1
View File
@@ -23,4 +23,3 @@ import subprocess
cmd = f'pyinstaller ./bootstrap.py --name ifcpatch --onefile --clean --add-data "ifcpatch{os.pathsep}ifcpatch"'
subprocess.check_output(cmd, shell=True)