Add documentation for IfcPatch and all recipes

This commit is contained in:
Dion Moult
2023-01-10 10:18:13 +11:00
parent c8ba96cde8
commit da84cc5925
23 changed files with 611 additions and 126 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ autoapi_add_toctree_entry = True
autoapi_type = 'python'
# autoapi works by reading source code instead of importing modules
autoapi_dirs = ['../ifcopenshell', '../../ifcdiff']
autoapi_dirs = ['../ifcopenshell', '../../ifcdiff', '../../ifcpatch/ifcpatch']
# These are auto-generated based on the IFC schema, so exclude them
autoapi_ignore = ['*ifcopenshell/express/rules*']
@@ -75,3 +75,8 @@ Alternatively, you can package it as an executable.
$ python make.py
$ ./dist/ifcpatch
Patch recipes
-------------
You can view all built-in patches in IfcPatch here: :doc:`List of IfcPatch recipes <autoapi/ifcpatch/recipes/index>`.
+49 -4
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, 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcPatch.
#
@@ -28,6 +28,41 @@ import importlib
def execute(args):
"""Execute a patch recipe
The details of how the patch recipe is executed depends on the definition of
the recipe, as well as the arguments passed to the recipe. See the
documentation for each patch recipe separately to understand more.
:param args: A dictionary of arguments, corresponding to the parameters
listed subsequent to this in this docstring.
:type args: dict
:param input: An IFC model to apply the patch recipe to.
:type input: ifcopenshell.file.file
:param recipe: The name of the recipe. This is the same as the filename of
the recipe. E.g. "ExtractElements".
:type recipe: str
:param log: A filepath to a logfile.
:type log: str,optional
:param arguments: A list of zero or more positional arguments, depending on
the patch recipe. Some patch recipes will require you to specify
arguments, some won't.
:type arguments: list
:return: The result of the patch. This is typically a patched model, either
as an object or as a string.
:rtype: ifcopenshell.file.file,str
Example:
.. code:: python
output = ifcpatch.execute({
"input": ifcopenshell.open("input.ifc"),
"recipe": "ExtractElements",
"arguments": [".IfcWall"],
})
ifcpatch.write(output, "output.ifc")
"""
if "log" in args:
logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG)
logger = logging.getLogger("IFCPatch")
@@ -40,13 +75,23 @@ def execute(args):
patcher = recipe.Patcher(args["input"], ifc_file, logger, args["arguments"])
patcher.patch()
output = getattr(patcher, "file_patched", patcher.file)
if isinstance(output, str):
with open(args["output"], "w") as text_file:
text_file.write(output)
return output
def write(output, filepath):
"""Write the output of an IFC patch to a file
Typically a patch output would be a patched IFC model file object, or as a
string. This function lets you agnostically write that output to a filepath.
:param output: The results from ifcpatch.execute()
:type output: ifcopenshell.file.file,str
:param filepath: A filepath to where the results of the patched model should
be written to.
:type filepath: str
:return: None
:rtype: None
"""
if isinstance(output, str):
with open(filepath, "w") as text_file:
text_file.write(output)
@@ -24,14 +24,32 @@ import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, unit="METERS"):
"""Converts the length unit of a model to the specified unit
Allowed metric units include METERS, MILLIMETERS, CENTIMETERS, etc.
Allowed imperial units include INCHES, FEET, MILES.
:param unit: The name of the desired unit.
:type unit: str
Example:
.. code:: python
# Convert to millimeters
ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["MILLIMETERS"]})
# Convert to feet
ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["FEET"]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.unit = unit
def patch(self):
unit = {"is_metric": "METERS" in self.args[0], "raw": self.args[0]}
unit = {"is_metric": "METERS" in self.unit, "raw": self.unit}
self.file_patched = ifcopenshell.api.run("project.create_file", version=self.file.schema)
if self.file.schema == "IFC2X3":
user = self.file_patched.add(self.file.by_type("IfcProject")[0].OwnerHistory.OwningUser)
@@ -22,19 +22,51 @@ import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, property_name=None, quantity_name=None):
"""Converts a property to a standardised quantity
IFC can store arbitrary key value metadata associated with a elements
known as properties and quantities. The difference between the two is
that quantities specifically measure physical dimensions, and can be
used parametrically. A collection of standardised quantities have also
been published by buildingSMART. Using standardised quantities can help
automate BIM workflows, instead of arbitrary properties.
Sometimes, proprietary BIM software incorrectly stores quantities and
properties. This patch lets you convert these incorrect properties to
standardised quantities instead.
The existing property will not be removed.
:param property_name: The name of the property to convert into a
quantity. The name of the property set is not considered.
:type property_name: str
:param quantity_name: The name of the quantity that this property should
be stored in. This should be a standard name that is one of the
quantity names of a buildingSMART quantity template. For example, it
may be "NetSideArea" for walls, which exists in
Qto_WallBaseQuantities. The quantity set name will be based on the
standard buildingSMART quantity template.
:type quantity_name: str
Example:
.. code:: python
# Any property named "Area" is converted to a quantity named
# "NetSideArea", if that standardised quantity exists.
ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": ["Area", "NetSideArea"]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.source_property_name = property_name
self.destination_quantity_name = quantity_name
def patch(self):
self.qto_template_cache = {}
self.psetqto = ifcopenshell.util.pset.get_template("IFC4")
self.source_property_name = self.args[0]
self.destination_quantity_name = self.args[1]
for product in self.file.by_type("IfcTypeProduct"):
self.process_product(product, product.HasPropertySets or [])
@@ -21,11 +21,27 @@ import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger):
"""Downgrade indexed polycurves to simple polylines
Low quality IFC viewers like Navisworks do not support various IFC4
geometry, such as indexed polycurves. These can result in missing
geometry or geometric glitches (such as arcs being displayed as full
circles). This is pretty common when viewing IFCs from ArchiCAD that
include site boundaries (incorrectly drawn using the ArchiCAD grid tool,
as ArchiCAD has no site boundary tool).
This will downgrade specifically the indexed polycurve geometry types in
an IFC4 model (IFC2X3 does not have this geometry type) to help
compatibility in viewers like Navisworks.
Example:
ifcpatch.execute({"input": model, "recipe": "DowngradeIndexedPolyCurve", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
if self.file.schema == "IFC2X3":
@@ -23,11 +23,27 @@ import ifcopenshell.util.selector
class Patcher:
def __init__(self, src, file, logger, query: str = ".IfcWall"):
"""Extract Elements
"""Extract certain elements into a new model
Extract a subset of elements from an existing IFC data set and save it to a new IFC file.
Extract a subset of elements from an existing IFC data set and save it
to a new IFC file. For example, you might want to extract only the walls
in a model and save it as a new model.
:param query: A query to select the subset of IFC elements.
:type query: str
Example:
.. code:: python
# Extract all walls
ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcWall"]})
# Extract all slabs
ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcSlab"]})
# Extract all walls and slabs
ifcpatch.execute({"input": model, "recipe": "ExtractElements", "arguments": [".IfcWall|.IfcSlab"]})
"""
self.src = src
self.file = file
@@ -1,28 +1,36 @@
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger):
"""Fix missing or spot-coordinate bugged TINs loading in Revit
TINs exported from 12D may contain dense or highly obtuse triangles.
Although these will load in Revit, you will not be able to use Revit's
Spot Coordinate or Spot Elevation tool.
See bug: https://github.com/Autodesk/revit-ifc/issues/511
The solution will merge vertices closer than 10mm to prevent dense
portions of the TIN at a minor sacrifice of surveying accuracy. It will
also triangulate all meshes to prevent non-coplanar surfaces, and delete
any obtuse triangles where one of their XY angles is less than 0.3
degrees. Therefore the result will contain some minor "holes" in the
TIN, but these holes will only be in dense triangles that Revit can't
handle anyway and won't affect most coordination tasks.
This patch is designed to only work on 12D IFC exports. It also requires
you to run it using Blender, as the geometric modification uses the
Blender geometry engine.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "Fix12DToRevitTINs", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
# TINs exported from 12D may contain dense or highly obtuse triangles.
# Although these will load in Revit, you will not be able to use Revit's
# Spot Coordinate or Spot Elevation tool.
#
# See bug: https://github.com/Autodesk/revit-ifc/issues/511
#
# The solution will merge vertices closer than 10mm to prevent dense
# portions of the TIN at a minor sacrifice of surveying accuracy. It
# will also triangulate all meshes to prevent non-coplanar surfaces, and
# delete any obtuse triangles where one of their XY angles is less than
# 0.3 degrees. Therefore the result will contain some minor "holes" in
# the TIN, but these holes will only be in dense triangles that Revit
# can't handle anyway and won't affect most coordination tasks.
#
# This patch is designed to only work on 12D IFC exports. It also
# requires you to run it using Blender, as the geometric modification
# uses the Blender geometry engine.
import bpy
import bmesh
import blenderbim.tool as tool
@@ -6,32 +6,40 @@ import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger):
"""Fix missing door swings in Revit when viewing ArchiCAD IFCs
ArchiCAD has the ability to store 2D data with objects like doors for
door swings. ArchiCAD's implementation is not 100% correct (using
footprint instead of annotation contexts), but otherwise not too
shabby.
Revit, however, is incapable of understanding this 2D representation.
Revit users linking in IFCs produced by ArchiCAD may experience the
following symptoms:
A. Invisible doors, and difficulty selecting doors
B. Invisible door swings, or only visible at particular view ranges
C. Weird arc shapes around doors
D. Extra lines around doors and walls
E. Cannot easily change visibility graphics of 2D vs 3D elements
F. Cannot view 2D data in a 3D view
This is caused by the perfect storm of Revit IFC bugs, which we will
work through methodically. For programmers interested in the details of
how we fix this, read the comments of the patch function.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "FixArchiCADToRevitDoorSwings", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
# ArchiCAD has the ability to store 2D data with objects like doors for
# door swings. ArchiCAD's implementation is not 100% correct (using
# footprint instead of annotation contexts), but otherwise not too
# shabby.
#
# Revit, however, is incapable of understanding this 2D representation.
# Revit users linking in IFCs produced by ArchiCAD may experience the
# following symptoms:
#
# A. Invisible doors, and difficulty selecting doors
# B. Invisible door swings, or only visible at particular view ranges
# C. Weird arc shapes around doors
# D. Extra lines around doors and walls
# E. Cannot easily change visibility graphics of 2D vs 3D elements
# F. Cannot view 2D data in a 3D view
#
# This is caused by the perfect storm of Revit IFC bugs, which we will
# work through methodically.
# Revit has the ability to switch between 3D representations and 2D
# representations (e.g. in plan view). It does this by detecting IFC
# representations that belong to either the Model Body representation
@@ -1,35 +1,43 @@
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger):
"""Allow ArchiCAD IFC spaces to open as Revit rooms
The underlying problem is that Revit does not bring in IFC spaces as
spaces / rooms in Revit when you link an IFC in Revit. This has been
broken for at least 3 years and counting. This is a problem typically
for ArchiCAD architects who want to send rooms to MEP folks using
Revit.
See bug: https://github.com/Autodesk/revit-ifc/issues/15
The solution is to open an IFC in Revit instead of linking it, which
will convert IFC spaces into Revit rooms. However, there are very
specific scenarios where Revit will convert these rooms, which have
been painstakingly reverse engineered through trial and error.
Firstly, the rooms should have a lower bound with a Z value matching
the Z value of the storey it is on. Secondly, although faceted breps
do work in some scenarios (I assume Revit has an internal topological
analysis tool), conversion to an extruded area solid yield much more
robust results. Finally, changing the Precision value to an obscene
number very strangely seems to cause a lot more rooms to be converted
successfully.
This patch is designed to only work on ArchiCAD IFC exports where the
only contents of the IFC is IFC space and `nothing else`. It also
requires you to run it using Blender, as the geometric modification
uses the Blender geometry engine.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "FixArchiCADToRevitSpaces", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
# The underlying problem is that Revit does not bring in IFC spaces as
# spaces / rooms in Revit when you link an IFC in Revit. This has been
# broken for at least 3 years and counting. This is a problem typically
# for ArchiCAD architects who want to send rooms to MEP folks using
# Revit.
#
# See bug: https://github.com/Autodesk/revit-ifc/issues/15
#
# The solution is to open an IFC in Revit instead of linking it, which
# will convert IFC spaces into Revit rooms. However, there are very
# specific scenarios where Revit will convert these rooms, which have
# been painstakingly reverse engineered through trial and error.
# Firstly, the rooms should have a lower bound with a Z value matching
# the Z value of the storey it is on. Secondly, although faceted breps
# do work in some scenarios (I assume Revit has an internal topological
# analysis tool), conversion to an extruded area solid yield much more
# robust results. Finally, changing the Precision value to an obscene
# number very strangely seems to cause a lot more rooms to be converted
# successfully.
#
# This patch is designed to only work on ArchiCAD IFC exports where the
# only contents of the IFC is IFC space and _nothing else_. It also
# requires you to run it using Blender, as the geometric modification
# uses the Blender geometry engine.
import bpy
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector, Matrix
@@ -21,17 +21,50 @@ import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, attribute="Tag"):
"""Merge duplicate element types via the Tag or another attribute
Revit is notorious for creating many duplicate element types. Element
types may be duplicated by being mirrored, such as doors, columns, etc,
or being certain MEP equipment. This means that even though you think
you might have only 3 door families and 3 door types in your door
schedule, your IFC might actually incorrectly store 6 or more door types.
Revit stores the Revit Element ID in the "Tag" attribute of all IFC
elements, so we can deduce that multiple IFC elements with the same Tag
attribute have been duplicated in IFC. This patch will merge them into a
single element type.
You may optionally specify your own attribute if you want to merge using
different criteria, such as "Name". For example, may Revit users
incorrectly have multiple types with the same name as workarounds to
overcome various Revit limitations. This is incorrect and this patch
will merge the types into a single type.
Occurrences of the type will be remapped to the merged type.
:param attribute: The name of the attribute to merge element types based
on. Typically this will be "Tag" as it stores the unique ID from the
proprietary BIM software.
:type attribute: str
Example:
.. code:: python
# Default behaviour of merging by Tag attribute
ifcpatch.execute({"input": model, "recipe": "MergeDuplicateTypes", "arguments": []})
# Explicitly say we want to merge based on the Name attribute
ifcpatch.execute({"input": model, "recipe": "MergeDuplicateTypes", "arguments": ["Name"]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.attribute = attribute
def patch(self):
if self.args:
key = self.args[0]
else:
key = "Tag"
key = self.attribute
keys = {}
for element_type in self.file.by_type("IfcTypeObject"):
original_type = keys.get(getattr(element_type, key), None)
+20 -3
View File
@@ -21,14 +21,31 @@ import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, filepath=None):
"""Merge two IFC models into one
Note that other than combining the two IfcProject elements into one, no
further processing will be done. This means that you may end up with
duplicate spatial hierarchies (i.e. 2 sites, 2 buildings, etc).
:param filepath: The filepath of the second IFC model to merge into the
first. The first model is already specified as the input to
IfcPatch.
:type filepath: str
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "MergeProject", "arguments": ["/path/to/model2.ifc"]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.filepath = filepath
def patch(self):
source = ifcopenshell.open(self.args[0])
source = ifcopenshell.open(self.filepath)
original_project = self.file.by_type("IfcProject")[0]
merged_project = self.file.add(source.by_type("IfcProject")[0])
for element in source:
+18 -3
View File
@@ -21,14 +21,29 @@ import ifcopenshell.util.schema
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, schema="IFC4"):
"""Migrate from one IFC version to another
Note that this is experimental and will try to preserve as much data as
possible. Upgrading to IFC4 is more stable than downgrading to IFC2X3.
:param schema: The schema identifier of the IFC version to migrate to.
:type schema: str
Example:
.. code:: python
# Upgrade an IFC2X3 model to IFC4
ifcpatch.execute({"input": model, "recipe": "Migrate", "arguments": ["IFC4"]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.schema = schema
def patch(self):
self.file_patched = ifcopenshell.file(schema=self.args[0])
self.file_patched = ifcopenshell.file(schema=self.schema)
migrator = ifcopenshell.util.schema.Migrator()
for element in self.file:
migrator.migrate(element, self.file_patched)
@@ -23,11 +23,70 @@ import ifcopenshell.util.placement
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, x=None, y=None, z=None, ax=None, ay=None, az=None):
"""Offset and rotate all object placements in a model
Every physical object in an IFC model has an object placement, a
matrix dictating where it is in XYZ space and its rotation.
Sometimes, models will have their models offset incorrectly into map
coordinates (i.e. very large coordinates) when they should be using
local coordinates, or vice versa, or simply be using wrong coordinates.
In some cases, models will even be rotated, especially with mixups where
Y is up instead of Z, coming from low quality BIM software.
This patch lets you translate, and optionally rotate (either rotate 2D
in plan view along the Z axis, or rotate in 3D across any axis) the
entire IFC model.
:param x: The X coordinate to offset by in project length units.
:type x: float
:param y: The Y coordinate to offset by in project length units.
:type y: float
:param z: The Z coordinate to offset by in project length units.
:type z: float
:param ax: An optional angle to rotate by. If only this angle is
specified, it is treated as the angle to rotate in plan view (i.e.
around the Z axis). If all angle parameters are specified, then it
is treated as the angle to rotate around the X axis. Angles are in
decimal degrees.
:type ax: float,optional
:param ay: An optional angle to rotate by for 3D rotations along the Y
axis. Angles are in decimal degrees.
:type ay: float,optional
:param az: An optional angle to rotate by for 3D rotations along the Z
axis. Angles are in decimal degrees.
:type az: float,optional
Example:
.. code:: python
# Offset a model by 100 units in both the X and Y axis.
ifcpatch.execute({"input": model, "recipe": "OffsetObjectPlacements", "arguments": [100,100,0]})
# Rotate by 90 degrees, but don't do any offset
ifcpatch.execute({"input": model, "recipe": "OffsetObjectPlacements", "arguments": [0,0,0,90]})
# Some crazy 3D rotation and offset
ifcpatch.execute({"input": model, "recipe": "OffsetObjectPlacements", "arguments": [12.5,5,2,90,90,45]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.x = x
self.y = y
self.z = z
self.ax = ax
self.ay = ay
self.az = az
if self.ay is not None:
self.angle_type = "3D"
elif self.ay is not None:
self.angle_type = "2D"
else:
self.angle_type = None
def patch(self):
absolute_placements = []
@@ -41,17 +100,17 @@ class Patcher:
absolute_placements = set(absolute_placements)
transformation = self.identity_matrix()
if len(self.args) == 4:
angle = float(self.args[3])
if self.angle_type == "2D":
angle = float(self.ax)
if angle:
transformation = self.z_rotation_matrix(math.radians(angle), transformation)
elif len(self.args) == 6:
for arg in (("x", float(self.args[3])), ("y", float(self.args[4])), ("z", float(self.args[5]))):
elif self.angle_type == "3D":
for arg in (("x", float(self.ax)), ("y", float(self.ay)), ("z", float(self.az))):
if arg[1]:
transformation = getattr(self, f"{arg[0]}_rotation_matrix")(math.radians(arg[1]), transformation)
transformation[0][3] += float(self.args[0])
transformation[1][3] += float(self.args[1])
transformation[2][3] += float(self.args[2])
transformation[0][3] += float(self.x)
transformation[1][3] += float(self.y)
transformation[2][3] += float(self.z)
for placement in absolute_placements:
placement.RelativePlacement = self.get_relative_placement(
@@ -18,18 +18,32 @@
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, z=None):
"""Offset building storeys by a particular Z value
All objects placed relative to the storeys will also be shifted.
:param z: The Z value in project length units to offset storeys by.
:type z: float
Example:
.. code:: python
# Shift all storeys up by 42 units
ifcpatch.execute({"input": model, "recipe": "OffsetStoreyElevations", "arguments": [42]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.z = z
def patch(self):
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.z))
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
+25 -2
View File
@@ -22,11 +22,34 @@ from toposort import toposort_flatten as toposort
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger):
"""Optimise the filesize of an IFC model
It is possible to non-losslessly optimise the filesize of an IFC model.
Note that this is usually not recommended. Optimising runs a risk of
losing some indirect semantic data critical for native IFC authoring.
Most parties who recommend optimisation are not aware of these risks.
Optimising is only safe in the context of read-only IFCs.
If filesize is an issue, another approach would be to use IFCZIP
instead to compress the model. Optimising the model only typically
affects filesize and has minimal impact on load times. Large filesizes
can usually be solved through other means. Consult the BlenderBIM Add-on
documentation on dealing with large models for more details.
Warning: this optimise recipe is very, very slow. Please consider using
RecycleNonRootedElements instead.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "Optimise", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.optimized_file = ifcopenshell.file(schema=self.file.schema)
def patch(self):
@@ -21,11 +21,33 @@ import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger):
"""Optimise the filesize of an IFC model by reusing non-rooted elements
It is possible to non-losslessly optimise the filesize of an IFC model.
Note that this is usually not recommended. Optimising runs a risk of
losing some indirect semantic data critical for native IFC authoring.
Most parties who recommend optimisation are not aware of these risks.
Optimising is only safe in the context of read-only IFCs.
If filesize is an issue, another approach would be to use IFCZIP
instead to compress the model. Optimising the model only typically
affects filesize and has minimal impact on load times. Large filesizes
can usually be solved through other means. Consult the BlenderBIM Add-on
documentation on dealing with large models for more details.
This patch may be run multiple times with diminishing returns.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "RecycleNonRootedElements", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
deleted = []
@@ -20,14 +20,39 @@ import ifcopenshell
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, only_duplicates=False):
"""Regenerate GlobalIds in an IFC model
All root elements in an IFC model must be identified by a unique Global
ID (also known as a GUID or UUID). Some proprietary BIM software do this
incorrect (I know right), either by generating an invalid ID, creating
duplicate IDs, generating IDs in a way that is not universally unique or
as random as you might prefer (e.g. non-compliant with UUID v4).
This will regenerate new GlobalIds for the entire model.
:param only_duplicates: If set to True, new GlobalIds will only be
generated for duplicate IDs. This is a safe thing to run to ensure
IFCs are valid. If False, all GlobalIds will be regenerated.
:type only_duplicates: bool
Example:
.. code:: python
# Regenerate all GlobalIds
ifcpatch.execute({"input": model, "recipe": "RegenerateGlobalIds", "arguments": []})
# Regenerate only duplicate GlobalIds
ifcpatch.execute({"input": model, "recipe": "RegenerateGlobalIds", "arguments": [True]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.only_duplicates = only_duplicates
def patch(self):
if self.args and self.args[0] == "DUPLICATE":
if self.only_duplicates:
guids = set()
for element in self.file.by_type("IfcRoot"):
if element.GlobalId in guids:
@@ -18,11 +18,20 @@
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger):
"""Removes any 3D geometry associated with a site or multiple sites
If no sites or no site geometry is present, nothing happens.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "RemoveSiteRepresentation", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
project = self.file.by_type("IfcProject")[0]
@@ -18,11 +18,76 @@
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, a=None, b=None, c=None, d=None):
"""Reset any large coordinates to smaller coordinates based on a threshold
If you find large coordinates in your model, the large coordinates may
either by due to large coordinates in the object placement matrix of
each object, or large coordinates in the geometry of each object.
If it is the former (preferred), consult the OffsetObjectPlacements
recipe. If it is the latter, this indicates seriously incorrect
coordinates in your IFC model.
This recipe finds any large coordinates (if either the X, Y, or Z
ordinate is larger than a threshold) and offsets it back down to a small
number.
You may either manually specify the offset to apply to any large
coordinate, or an offset will be automatically determined arbitrarily
based on the first large number we encounter.
Note that if your model inconsistently mixes coordinates between large
and small (such as if your model mixes both local and map coordinates)
then the results of this function may be poor. Provide a bug report back
to your BIM application to get it fixed.
You may specify up to 4 arguments, a, b, c, and d.
If you specify no arguments, then the threshold is set to 1000000. The
offset is auto detected.
If you only specify 1 parameter (i.e. a), then this is treated as the
threshold beyond which an ordinate is considered to be large. The offset
is auto detected.
If you specify 3 parameters, (i.e. a, b, c) then your three numbers are
treated as the X, Y, Z offset to apply. Typically your numbers will be
negative to bring the numbers smaller. The threshold is set to 1000000.
If you specify 4 parameters (i.e. a, b, c, d), then the first three
numbers are treated as the X, Y, Z offset to apply (a, b, c). The fourth
(d) will be treated as the threshold.
:param a: The first parameter
:type a: float,optional
:param b: The second parameter
:type b: float,optional
:param c: The third parameter
:type c: float,optional
:param d: The fourth parameter
:type d: float,optional
Example:
.. code:: python
# Reset all coordinates with an ordinate larger than 1000000 arbitrarily
ifcpatch.execute({"input": model, "recipe": "ResetAbsoluteCoordinates", "arguments": []})
# Reset all coordinates with an ordinate larger than 1000 arbitrarily
ifcpatch.execute({"input": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [1000]})
# Reset all coordinates with an ordinate larger than 1000000 by -50000,-20000,0
ifcpatch.execute({"input": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [-50000,-20000,0]})
# Reset all coordinates with an ordinate larger than 1000 by -500,-200,0
ifcpatch.execute({"input": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [-500,-200,0,1000]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.args = [x for x in [a, b, c, d] if x is not None]
def patch(self):
placement_coord_ids = set()
@@ -18,15 +18,31 @@
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, ifc_class="IfcSite"):
"""Resets the location of a spatial element to 0,0,0
Another more specialised patch to fix incorrect coordinate usage is to
reset the location of spatial elements (sites, buildings, storeys) back
to 0,0,0.
:param ifc_class: The class of spatial element to reset coordinates for.
:type ifc_class: str
Example:
.. code:: python
# All IfcSites will shift back to 0,0,0.
ifcpatch.execute({"input": model, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcSite"]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.ifc_class = ifc_class
def patch(self):
project = self.file.by_type("IfcProject")[0]
spatial_elements = self.find_decomposed_ifc_class(project, self.args[0])
spatial_elements = self.find_decomposed_ifc_class(project, self.ifc_class)
for spatial_element in spatial_elements:
self.patch_placement_to_origin(spatial_element)
@@ -18,17 +18,37 @@
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger, elevation=0):
"""Sets the reference elevation of all IfcSites
To completely reference model coordinates, a reference elevation should
be specified on the IfcSite. This is often omitted or not possible by
proprietary BIM applications. This patch lets you set the reference
elevation attribute explicitly.
Note that this does not physically shift the Z coordinates of anything.
The reference elevation is simply a numerical attribute.
:param elevation: The elevation to set.
:type elevation: float
Example:
.. code:: python
# All IfcSites will have their reference elevation set to 42.
ifcpatch.execute({"input": model, "recipe": "SetRefElevation", "arguments": [42]})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
self.elevation = elevation
def patch(self):
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])
site.RefElevation = float(self.elevation)
def find_decomposed_ifc_class(self, element, ifc_class):
results = []
@@ -18,11 +18,22 @@
class Patcher:
def __init__(self, src, file, logger, args=None):
def __init__(self, src, file, logger):
"""Split an IFC model into multiple models based on building storey
The new IFC model names will be named after the storey name in the
format of {i}-{name}.ifc, where {i} is an ascending number starting from
0 and {name} is the name of the storey.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "SplitByBuildingStorey", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.args = args
def patch(self):
import ifcopenshell