diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py
index a6abb3e7f3..594755685c 100644
--- a/src/ifcpatch/ifcpatch/__init__.py
+++ b/src/ifcpatch/ifcpatch/__init__.py
@@ -27,13 +27,22 @@ import inspect
import collections
import importlib
import importlib.util
-from typing import Union, Iterable, Optional, Any
+from typing import Union, Iterable, Optional, Any, TypedDict, Literal
+from typing_extensions import NotRequired
__version__ = version = "0.0.0"
-def execute(args: dict) -> Union[ifcopenshell.file, str]:
+class ArgumentsDict(TypedDict):
+ recipe: str
+ file: NotRequired[ifcopenshell.file]
+ input: NotRequired[str]
+ log: NotRequired[str]
+ arguments: NotRequired[list]
+
+
+def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
"""Execute a patch recipe
The details of how the patch recipe is executed depends on the definition of
@@ -43,10 +52,13 @@ def execute(args: dict) -> Union[ifcopenshell.file, str]:
:param args: A dictionary of arguments, corresponding to the parameters
listed subsequent to this in this docstring.
:type args: dict
- :param input: A filepath to the incoming IFC file.
- :type input: str
:param file: An IFC model to apply the patch recipe to.
- :type file: ifcopenshell.file.file
+ Required for most recipes except the ones that require `input`.
+ :type file: ifcopenshell.file, optional
+ :param input: A filepath to the incoming IFC file.
+ Required/supported only for some recipes, see specific recipes descriptions,
+ in other cases will be ignored.
+ :type input: str, optional
:param recipe: The name of the recipe. This is the same as the filename of
the recipe. E.g. "ExtractElements".
:type recipe: str
@@ -58,7 +70,6 @@ def execute(args: dict) -> Union[ifcopenshell.file, str]:
: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:
@@ -77,16 +88,32 @@ def execute(args: dict) -> Union[ifcopenshell.file, str]:
logger = logging.getLogger("IFCPatch")
recipes = getattr(__import__("ifcpatch.recipes.{}".format(args["recipe"])), "recipes")
recipe = getattr(recipes, args["recipe"])
+
+ # Ensure file or input is provided.
+ input_argument = get_patch_input_argument_use(args["recipe"])
+ if input_argument == "REQUIRED":
+ if not args.get("input"):
+ raise ValueError(f"Recipe {args['recipe']} is requiring 'input' argument to be provided.")
+ elif "file" not in args: # SUPPORTED, IGNORED.
+ raise ValueError(f"Recipe {args['recipe']} is requiring 'file' argument to be provided.")
+
arguments = args.get("arguments", [])
if recipe.Patcher.__init__.__doc__ is not None:
- patcher = recipe.Patcher(args.get("input"), args["file"], logger, *arguments)
+ patcher = recipe.Patcher(args.get("input"), args.get("file"), logger, *arguments)
else:
- patcher = recipe.Patcher(args.get("input"), args["file"], logger, arguments)
+ patcher = recipe.Patcher(args.get("input"), args.get("file"), logger, arguments)
patcher.patch()
output = getattr(patcher, "file_patched", patcher.file)
return output
+def get_patch_input_argument_use(recipe: str) -> Literal["REQUIRED", "SUPPORTED", "IGNORED"]:
+ import importlib
+
+ recipe_module = importlib.import_module(f"ifcpatch.recipes.{recipe}")
+ return getattr(recipe_module.Patcher, "input_argument", "IGNORED")
+
+
def write(output: Union[ifcopenshell.file, str], filepath: str) -> None:
"""Write the output of an IFC patch to a file
diff --git a/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py b/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py
index c65cdc0356..49b6507a7e 100644
--- a/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py
+++ b/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py
@@ -16,9 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see .
+import logging
+import ifcopenshell
+
class Patcher:
- def __init__(self, src, file, logger):
+ input_argument = "REQUIRED"
+
+ def __init__(self, src: str, file: None, logger: logging.Logger):
"""Allow ArchiCAD IFC spaces to open as Revit rooms
The underlying problem is that Revit does not bring in IFC spaces as
@@ -46,17 +51,18 @@ class Patcher:
requires you to run it using Blender, as the geometric modification
uses the Blender geometry engine.
+ `input` argument is required for this recipe, `file` argument is ignored.
+
Example:
.. code:: python
-
- ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixArchiCADToRevitSpaces", "arguments": []})
+ ifcpatch.execute({"input": "input.ifc", "recipe": "FixArchiCADToRevitSpaces", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
- def patch(self):
+ def patch(self) -> None:
import bpy
import bonsai.tool as tool
import ifcopenshell
@@ -70,10 +76,11 @@ class Patcher:
bpy.ops.bim.load_project(filepath=self.src)
- def recalculate_origin(wall):
+ def recalculate_origin(wall: bpy.types.Object) -> None:
new_origin = wall.matrix_world @ Vector(wall.bound_box[0])
if (wall.matrix_world.translation - new_origin).length < 0.001:
return
+ assert isinstance(wall.data, bpy.types.Mesh)
wall.data.transform(
Matrix.Translation(
(wall.matrix_world.inverted().to_quaternion() @ (wall.matrix_world.translation - new_origin))
diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py
index f2ec982aa4..60a08dd876 100644
--- a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py
+++ b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py
@@ -17,8 +17,14 @@
# along with IfcPatch. If not, see .
+import ifcopenshell
+import logging
+
+
class Patcher:
- def __init__(self, src, file, logger, is_solid=True):
+ input_argument = "REQUIRED"
+
+ def __init__(self, src: str, file: None, logger: logging.Logger, is_solid: bool = True):
"""Fix missing or spot-coordinate bugged TINs loading in Revit
TINs exported from 12D or Civil 3D may contain dense or highly obtuse
@@ -56,18 +62,20 @@ class Patcher:
from civil software. It also requires you to run it using Blender, as
the geometric modification uses the Blender geometry engine.
+ `input` argument is required for this recipe, `file` argument is ignored.
+
Example:
.. code:: python
- ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixRevitTINs", "arguments": []})
+ ifcpatch.execute({"input": "input.ifc", "recipe": "FixRevitTINs", "arguments": []})
"""
self.src = src
self.file = file
self.logger = logger
self.is_solid = is_solid
- def patch(self):
+ def patch(self) -> None:
import bpy
import bmesh
import bonsai.tool as tool
diff --git a/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py b/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py
index c8b8b1cd51..82f4e32fc4 100644
--- a/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py
+++ b/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py
@@ -24,6 +24,8 @@ from typing import Union
class Patcher:
+ input_argument = "SUPPORTED"
+
def __init__(self, src: str, file: ifcopenshell.file, logger: logging.Logger, output_dir: Union[str, None] = None):
"""Split an IFC model into multiple models based on building storey
@@ -31,6 +33,9 @@ class Patcher:
format of {i}-{name}.ifc, where {i} is an ascending number starting from
0 and {name} is the name of the storey.
+ `input` argument might be provided to ifcpatch - it will be used load file from disk
+ (otherwise `file` will be saved to a temporary file).
+
:param output_dir: Specifies an output directory where the new IFC models will be saved.
Example: