mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 02:47:48 +00:00
Add documentation for IfcPatch and all recipes
This commit is contained in:
@@ -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>`.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user