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: