Fix #5939. Fix #2751. IfcPatch no longer needs the (rarely used) input src arg by default.

This commit is contained in:
Dion Moult
2025-01-10 16:59:45 +11:00
parent 83add56e6c
commit cac3d7f699
33 changed files with 58 additions and 119 deletions
@@ -98,12 +98,6 @@ class ExecuteIfcPatch(bpy.types.Operator):
if props.should_load_from_memory and tool.Ifc.get(): if props.should_load_from_memory and tool.Ifc.get():
args["file"] = tool.Ifc.get() args["file"] = tool.Ifc.get()
if ifcpatch.get_patch_input_argument_use(recipe_name) == "REQUIRED":
self.report(
{"ERROR"},
f"The recipe '{recipe_name}' is not currently supported if file is loaded from memory.",
)
return {"CANCELLED"}
else: else:
args["input"] = cast(str, props.ifc_patch_input) args["input"] = cast(str, props.ifc_patch_input)
args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input)) args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input))
+2 -4
View File
@@ -33,10 +33,8 @@ class Patch(bonsai.core.tool.Patch):
@classmethod @classmethod
def is_filepath_argument(cls, recipe: str, arg_name: str) -> bool: def is_filepath_argument(cls, recipe: str, arg_name: str) -> bool:
# TODO: Temporary hack to identify filepath arguments. # There is probably a more explicit way to do this
# Should mark them as such in the patches documentation return "filepath" in arg_name
# and process it later.
return recipe == "SplitByBuildingStorey" and arg_name == "output_dir"
@classmethod @classmethod
def does_patch_has_output(cls, recipe: str) -> bool: def does_patch_has_output(cls, recipe: str) -> bool:
+2 -16
View File
@@ -95,30 +95,16 @@ def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
else: else:
recipe = importlib.import_module(f"ifcpatch.recipes.{args['recipe']}") recipe = importlib.import_module(f"ifcpatch.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", None) or [] arguments = args.get("arguments", None) or []
if recipe.Patcher.__init__.__doc__ is not None: if recipe.Patcher.__init__.__doc__ is not None:
patcher = recipe.Patcher(args.get("input"), args.get("file"), logger, *arguments) patcher = recipe.Patcher(args.get("file"), logger, *arguments)
else: else:
patcher = recipe.Patcher(args.get("input"), args.get("file"), logger, arguments) patcher = recipe.Patcher(args.get("file"), logger, arguments)
patcher.patch() patcher.patch()
output = getattr(patcher, "file_patched", patcher.file) output = getattr(patcher, "file_patched", patcher.file)
return output return output
def get_patch_input_argument_use(recipe: str) -> Literal["REQUIRED", "SUPPORTED", "IGNORED"]:
# try out of tree and subpackage imports
recipe_module = sys.modules.get(f"ifcpatch.recipes.{recipe}") or sys.modules.get(recipe)
return getattr(recipe_module.Patcher, "input_argument", "IGNORED")
def write(output: Union[ifcopenshell.file, str], filepath: str) -> None: def write(output: Union[ifcopenshell.file, str], filepath: str) -> None:
"""Write the output of an IFC patch to a file """Write the output of an IFC patch to a file
@@ -53,7 +53,6 @@ LengthUnit = typing.Literal[
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src: str,
file: ifcopenshell.file, file: ifcopenshell.file,
logger: Logger, logger: Logger,
unit: LengthUnit = "METER", unit: LengthUnit = "METER",
@@ -76,7 +75,6 @@ class Patcher:
# Convert to feet # Convert to feet
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]}) model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.unit = unit self.unit = unit
@@ -21,7 +21,7 @@ import ifcopenshell.guid
class Patcher: class Patcher:
def __init__(self, src, file, logger): def __init__(self, file, logger):
"""Convert nesting relationships to aggregate relationships """Convert nesting relationships to aggregate relationships
Some software like Revit won't load nested children elements because Some software like Revit won't load nested children elements because
@@ -39,7 +39,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertNestToAggregate", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertNestToAggregate", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -26,7 +26,7 @@ from typing import Union
class Patcher: class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, property_name: str, quantity_name: str): def __init__(self, file: ifcopenshell.file, logger: Logger, property_name: str, quantity_name: str):
"""Converts a property to a standardised quantity """Converts a property to a standardised quantity
IFC can store arbitrary key value metadata associated with a elements IFC can store arbitrary key value metadata associated with a elements
@@ -59,7 +59,6 @@ class Patcher:
# "NetSideArea", if that standardised quantity exists. # "NetSideArea", if that standardised quantity exists.
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertPropertiesToQuantities", "arguments": ["Area", "NetSideArea"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertPropertiesToQuantities", "arguments": ["Area", "NetSideArea"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.source_property_name = property_name self.source_property_name = property_name
@@ -21,7 +21,7 @@ import ifcopenshell.util.element
class Patcher: class Patcher:
def __init__(self, src, file, logger): def __init__(self, file, logger):
"""Downgrade indexed polycurves to simple polylines """Downgrade indexed polycurves to simple polylines
Low quality IFC viewers like Navisworks do not support various IFC4 Low quality IFC viewers like Navisworks do not support various IFC4
@@ -39,7 +39,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "DowngradeIndexedPolyCurve", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "DowngradeIndexedPolyCurve", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -25,7 +25,7 @@ from logging import Logger
class Patcher: class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, query: str = "IfcWall"): def __init__(self, file: ifcopenshell.file, logger: Logger, query: str = "IfcWall"):
"""Extract certain elements into a new model """Extract certain elements into a new model
Extract a subset of elements from an existing IFC data set and save it Extract a subset of elements from an existing IFC data set and save it
@@ -48,7 +48,6 @@ class Patcher:
# Extract all walls and slabs # Extract all walls and slabs
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.query = query self.query = query
@@ -34,7 +34,6 @@ except:
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src,
file, file,
logger, logger,
): ):
@@ -50,7 +49,6 @@ class Patcher:
result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"}) result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"})
ifcpatch.write(result, "output.sqlite") ifcpatch.write(result, "output.sqlite")
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -27,7 +27,7 @@ import ifcopenshell.util.element
class Patcher: class Patcher:
def __init__(self, src, file, logger): def __init__(self, file, logger):
"""Fix missing door swings in Revit when viewing ArchiCAD IFCs """Fix missing door swings in Revit when viewing ArchiCAD IFCs
ArchiCAD has the ability to store 2D data with objects like doors for ArchiCAD has the ability to store 2D data with objects like doors for
@@ -56,7 +56,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixArchiCADToRevitDoorSwings", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixArchiCADToRevitDoorSwings", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -21,9 +21,7 @@ import ifcopenshell
class Patcher: class Patcher:
input_argument = "REQUIRED" def __init__(self, file: None, logger: logging.Logger, filepath: str):
def __init__(self, src: str, file: None, logger: logging.Logger):
"""Allow ArchiCAD IFC spaces to open as Revit rooms """Allow ArchiCAD IFC spaces to open as Revit rooms
The underlying problem is that Revit does not bring in IFC spaces as The underlying problem is that Revit does not bring in IFC spaces as
@@ -51,16 +49,21 @@ class Patcher:
requires you to run it using Blender, as the geometric modification requires you to run it using Blender, as the geometric modification
uses the Blender geometry engine. uses the Blender geometry engine.
`input` argument is required for this recipe, `file` argument is ignored. `filepath` argument is required for this recipe, `file` argument is
ignored.
:param filepath: The filepath of the IFC model. This is required to
load into Bonsai.
:filter_glob filepath: *.ifc;*.ifczip;*.ifcxml
Example: Example:
.. code:: python .. code:: python
ifcpatch.execute({"input": "input.ifc", "recipe": "FixArchiCADToRevitSpaces", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "recipe": "FixArchiCADToRevitSpaces", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.filepath = filepath
def patch(self) -> None: def patch(self) -> None:
import bpy import bpy
@@ -74,7 +77,7 @@ class Patcher:
bpy.data.batch_remove(bpy.data.objects) bpy.data.batch_remove(bpy.data.objects)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
bpy.ops.bim.load_project(filepath=self.src) bpy.ops.bim.load_project(filepath=self.filepath)
def recalculate_origin(wall: bpy.types.Object) -> None: def recalculate_origin(wall: bpy.types.Object) -> None:
new_origin = wall.matrix_world @ Vector(wall.bound_box[0]) new_origin = wall.matrix_world @ Vector(wall.bound_box[0])
@@ -21,7 +21,7 @@ import ifcopenshell.util.element
class Patcher: class Patcher:
def __init__(self, src, file, logger): def __init__(self, file, logger):
"""Reassigns occurrence classifications to types """Reassigns occurrence classifications to types
Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/691) Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/691)
@@ -35,7 +35,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixRevitClassificationCodeTypes"}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixRevitClassificationCodeTypes"})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -22,9 +22,7 @@ import logging
class Patcher: class Patcher:
input_argument = "REQUIRED" def __init__(self, file: None, logger: logging.Logger, filepath: str, is_solid: bool = True):
def __init__(self, src: str, file: None, logger: logging.Logger, is_solid: bool = True):
"""Fix missing or spot-coordinate bugged TINs loading in Revit """Fix missing or spot-coordinate bugged TINs loading in Revit
TINs exported from 12D or Civil 3D may contain dense or highly obtuse TINs exported from 12D or Civil 3D may contain dense or highly obtuse
@@ -62,7 +60,12 @@ class Patcher:
from civil software. It also requires you to run it using Blender, as from civil software. It also requires you to run it using Blender, as
the geometric modification uses the Blender geometry engine. the geometric modification uses the Blender geometry engine.
`input` argument is required for this recipe, `file` argument is ignored. `filepath` argument is required for this recipe, `file` argument is
ignored.
:param filepath: The filepath of the IFC model. This is required to
load into Bonsai.
:filter_glob filepath: *.ifc;*.ifczip;*.ifcxml
Example: Example:
@@ -70,8 +73,8 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "recipe": "FixRevitTINs", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "recipe": "FixRevitTINs", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.filepath = filepath
self.logger = logger self.logger = logger
self.is_solid = is_solid self.is_solid = is_solid
@@ -82,7 +85,7 @@ class Patcher:
from math import degrees from math import degrees
bpy.context.scene.BIMProjectProperties.should_use_native_meshes = True bpy.context.scene.BIMProjectProperties.should_use_native_meshes = True
bpy.ops.bim.load_project(filepath=self.src) bpy.ops.bim.load_project(filepath=self.filepath)
old_history_size = tool.Ifc.get().history_size old_history_size = tool.Ifc.get().history_size
old_undo_steps = bpy.context.preferences.edit.undo_steps old_undo_steps = bpy.context.preferences.edit.undo_steps
-2
View File
@@ -54,7 +54,6 @@ except:
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src,
file, file,
logger, logger,
sql_type: SQLTypes = "SQLite", sql_type: SQLTypes = "SQLite",
@@ -98,7 +97,6 @@ class Patcher:
{"input": "input.ifc", "file": model, "recipe": "Ifc2Sql", "arguments": ["sqlite"]} {"input": "input.ifc", "file": model, "recipe": "Ifc2Sql", "arguments": ["sqlite"]}
) )
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.sql_type = sql_type.lower() self.sql_type = sql_type.lower()
@@ -23,7 +23,7 @@ from logging import Logger
class Patcher: class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, attribute: str = "Tag"): def __init__(self, file: ifcopenshell.file, logger: Logger, attribute: str = "Tag"):
"""Merge duplicate element types via the Tag or another attribute """Merge duplicate element types via the Tag or another attribute
Revit is notorious for creating many duplicate element types. Element Revit is notorious for creating many duplicate element types. Element
@@ -60,7 +60,6 @@ class Patcher:
# Explicitly say we want to merge based on the Name attribute # Explicitly say we want to merge based on the Name attribute
ifcpatch.execute({"file": model, "recipe": "MergeDuplicateTypes", "arguments": ["Name"]}) ifcpatch.execute({"file": model, "recipe": "MergeDuplicateTypes", "arguments": ["Name"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.attribute = attribute self.attribute = attribute
@@ -27,9 +27,7 @@ from logging import Logger
class Patcher: class Patcher:
def __init__( def __init__(self, file: ifcopenshell.file, logger: Logger, filepaths: list[Union[str, ifcopenshell.file]]):
self, src: str, file: ifcopenshell.file, logger: Logger, filepaths: list[Union[str, ifcopenshell.file]]
):
"""Merge two or more IFC models into one """Merge two or more IFC models into one
Note that other than combining the two (or more) IfcProject elements into Note that other than combining the two (or more) IfcProject elements into
@@ -50,7 +48,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "MergeProjects", "arguments": ["/path/to/model2.ifc"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "MergeProjects", "arguments": ["/path/to/model2.ifc"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.filepaths = filepaths self.filepaths = filepaths
-2
View File
@@ -25,7 +25,6 @@ from logging import Logger
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src: str,
file: ifcopenshell.file, file: ifcopenshell.file,
logger: Logger, logger: Logger,
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4", schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
@@ -45,7 +44,6 @@ class Patcher:
# Upgrade an IFC2X3 model to IFC4 # Upgrade an IFC2X3 model to IFC4
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Migrate", "arguments": ["IFC4"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Migrate", "arguments": ["IFC4"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.schema = schema self.schema = schema
@@ -26,7 +26,6 @@ import typing
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src,
file, file,
logger, logger,
x: typing.Union[str, float] = "0", x: typing.Union[str, float] = "0",
@@ -89,7 +88,6 @@ class Patcher:
# Some crazy 3D rotation and offset # Some crazy 3D rotation and offset
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "OffsetObjectPlacements", "arguments": [12.5,5,2,False,90,90,45]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "OffsetObjectPlacements", "arguments": [12.5,5,2,False,90,90,45]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.x = float(x) self.x = float(x)
@@ -20,7 +20,7 @@ import typing
class Patcher: class Patcher:
def __init__(self, src, file, logger, z: typing.Union[str, float] = "0"): def __init__(self, file, logger, z: typing.Union[str, float] = "0"):
"""Offset building storeys by a particular Z value """Offset building storeys by a particular Z value
All objects placed relative to the storeys will also be shifted. All objects placed relative to the storeys will also be shifted.
@@ -35,7 +35,6 @@ class Patcher:
# Shift all storeys up by 42 units # Shift all storeys up by 42 units
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "OffsetStoreyElevations", "arguments": [42]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "OffsetStoreyElevations", "arguments": [42]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.z = float(z) self.z = float(z)
+1 -2
View File
@@ -21,7 +21,7 @@ import ifcopenshell.util.element
class Patcher: class Patcher:
def __init__(self, src, file, logger): def __init__(self, file, logger):
"""Optimise the filesize of an IFC model """Optimise the filesize of an IFC model
It is possible to non-losslessly optimise the filesize of an IFC model. It is possible to non-losslessly optimise the filesize of an IFC model.
@@ -46,7 +46,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Optimise", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Optimise", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.optimized_file = ifcopenshell.file(schema=self.file.schema) self.optimized_file = ifcopenshell.file(schema=self.file.schema)
+1 -2
View File
@@ -22,7 +22,7 @@ from logging import Logger
class Patcher: class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger): def __init__(self, file: ifcopenshell.file, logger: Logger):
"""Purge IFC properties, relationships, and other data """Purge IFC properties, relationships, and other data
In some rare cases (i.e. "resetting" a model or for security purposes) In some rare cases (i.e. "resetting" a model or for security purposes)
@@ -51,7 +51,6 @@ class Patcher:
# Watch the world burn # Watch the world burn
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "PurgeData", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "PurgeData", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -22,7 +22,7 @@ import ifcopenshell.util.element
class Patcher: class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: logging.Logger): def __init__(self, file: ifcopenshell.file, logger: logging.Logger):
"""Optimise the filesize of an IFC model by reusing non-rooted elements """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. It is possible to non-losslessly optimise the filesize of an IFC model.
@@ -46,7 +46,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RecycleNonRootedElements", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RecycleNonRootedElements", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -22,7 +22,7 @@ from logging import Logger
class Patcher: class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, only_duplicates=False): def __init__(self, file: ifcopenshell.file, logger: Logger, only_duplicates=False):
"""Regenerate GlobalIds in an IFC model """Regenerate GlobalIds in an IFC model
All root elements in an IFC model must be identified by a unique Global All root elements in an IFC model must be identified by a unique Global
@@ -48,7 +48,6 @@ class Patcher:
# Regenerate only duplicate GlobalIds # Regenerate only duplicate GlobalIds
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RegenerateGlobalIds", "arguments": [True]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RegenerateGlobalIds", "arguments": [True]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.only_duplicates = only_duplicates self.only_duplicates = only_duplicates
@@ -18,7 +18,7 @@
class Patcher: class Patcher:
def __init__(self, src, file, logger): def __init__(self, file, logger):
"""Removes the built-in Revit Uniformat classification. """Removes the built-in Revit Uniformat classification.
Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/486) Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/486)
@@ -31,7 +31,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RemoveRevitUniformatClassification"}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RemoveRevitUniformatClassification"})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -18,7 +18,7 @@
class Patcher: class Patcher:
def __init__(self, src, file, logger): def __init__(self, file, logger):
"""Removes any 3D geometry associated with a site or multiple sites """Removes any 3D geometry associated with a site or multiple sites
If no sites or no site geometry is present, nothing happens. If no sites or no site geometry is present, nothing happens.
@@ -29,7 +29,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RemoveSiteRepresentation", "arguments": []}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "RemoveSiteRepresentation", "arguments": []})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
@@ -26,7 +26,6 @@ from typing import Literal, Optional, Union
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src: str,
file: ifcopenshell.file, file: ifcopenshell.file,
logger: logging.Logger, logger: logging.Logger,
mode: Literal[ mode: Literal[
@@ -99,7 +98,6 @@ class Patcher:
# Reset all coordinates with an ordinate larger than 1000 by -500,-200,0 # Reset all coordinates with an ordinate larger than 1000 by -500,-200,0
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [False, 1000, -500,-200,0]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [False, 1000, -500,-200,0]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.mode = mode.lower() self.mode = mode.lower()
@@ -18,7 +18,7 @@
class Patcher: class Patcher:
def __init__(self, src, file, logger, ifc_class="IfcSite"): def __init__(self, file, logger, ifc_class="IfcSite"):
"""Resets the location of a spatial element to 0,0,0 """Resets the location of a spatial element to 0,0,0
Another more specialised patch to fix incorrect coordinate usage is to Another more specialised patch to fix incorrect coordinate usage is to
@@ -35,7 +35,6 @@ class Patcher:
# All IfcSites will shift back to 0,0,0. # All IfcSites will shift back to 0,0,0.
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcSite"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcSite"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.ifc_class = ifc_class self.ifc_class = ifc_class
@@ -25,7 +25,6 @@ import typing
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src,
file, file,
logger, logger,
name: str = "EPSG:1234", name: str = "EPSG:1234",
@@ -59,7 +58,6 @@ class Patcher:
# Set the current origin 0,0,0 to correlate to map coordinates 1000,1000,0 and a grid north of 15. # Set the current origin 0,0,0 to correlate to map coordinates 1000,1000,0 and a grid north of 15.
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetFalseOrigin", "arguments": ["EPSG:1234", 0, 0, 0, 1000, 1000, 0, 15, 0]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetFalseOrigin", "arguments": ["EPSG:1234", 0, 0, 0, 1000, 1000, 0, 15, 0]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.name = name self.name = name
@@ -73,7 +71,7 @@ class Patcher:
self.rotate_angle = float(rotate_angle) self.rotate_angle = float(rotate_angle)
def patch(self): def patch(self):
SetWorldCoordinateSystem.Patcher(self.src, self.file, self.logger, x=0, y=0, z=0, ax=0, ay=0, az=0).patch() SetWorldCoordinateSystem.Patcher(self.file, self.logger, x=0, y=0, z=0, ax=0, ay=0, az=0).patch()
coordinate_operation = { coordinate_operation = {
"Eastings": self.e, "Eastings": self.e,
"Northings": self.n, "Northings": self.n,
@@ -90,7 +88,6 @@ class Patcher:
self.file, projected_crs={"Name": self.name}, coordinate_operation=coordinate_operation self.file, projected_crs={"Name": self.name}, coordinate_operation=coordinate_operation
) )
OffsetObjectPlacements.Patcher( OffsetObjectPlacements.Patcher(
self.src,
self.file, self.file,
self.logger, self.logger,
x=-self.x, x=-self.x,
@@ -20,7 +20,7 @@ import typing
class Patcher: class Patcher:
def __init__(self, src, file, logger, elevation: typing.Union[str, float] = "0"): def __init__(self, file, logger, elevation: typing.Union[str, float] = "0"):
"""Sets the reference elevation of all IfcSites """Sets the reference elevation of all IfcSites
To completely reference model coordinates, a reference elevation should To completely reference model coordinates, a reference elevation should
@@ -41,7 +41,6 @@ class Patcher:
# All IfcSites will have their reference elevation set to 42. # All IfcSites will have their reference elevation set to 42.
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetRefElevation", "arguments": [42]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetRefElevation", "arguments": [42]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.elevation = float(elevation) self.elevation = float(elevation)
@@ -24,7 +24,6 @@ import typing
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src,
file, file,
logger, logger,
x: typing.Union[str, float] = "0", x: typing.Union[str, float] = "0",
@@ -61,7 +60,6 @@ class Patcher:
# Set the world coordinate system back to 0, 0, 0 # Set the world coordinate system back to 0, 0, 0
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetWorldCoordinateSystem", "arguments": [0,0,0,0,0,0]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SetWorldCoordinateSystem", "arguments": [0,0,0,0,0,0]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.x = float(x) self.x = float(x)
@@ -24,18 +24,13 @@ from typing import Union
class Patcher: class Patcher:
input_argument = "SUPPORTED" def __init__(self, file: ifcopenshell.file, logger: logging.Logger, output_dir: Union[str, None] = None):
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 """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 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 format of {i}-{name}.ifc, where {i} is an ascending number starting from
0 and {name} is the name of the storey. 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. :param output_dir: Specifies an output directory where the new IFC models will be saved.
Example: Example:
@@ -44,7 +39,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SplitByBuildingStorey", "arguments": ["C:/.../output_files"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "SplitByBuildingStorey", "arguments": ["C:/.../output_files"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.output_dir = output_dir self.output_dir = output_dir
@@ -60,17 +54,14 @@ class Patcher:
output_dir = Path(self.output_dir) output_dir = Path(self.output_dir)
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
temp_file = None temp_file = tempfile.NamedTemporaryFile(suffix=".ifc", delete=False)
if not self.src: self.file.write(temp_file.name)
temp_file = tempfile.NamedTemporaryFile(suffix=".ifc", delete=False)
self.src = temp_file.name
self.file.write(self.src)
storeys = self.file.by_type("IfcBuildingStorey") storeys = self.file.by_type("IfcBuildingStorey")
for i, storey in enumerate(storeys): for i, storey in enumerate(storeys):
filename = f"{i}-{storey.Name}.ifc" filename = f"{i}-{storey.Name}.ifc"
dest = filename if output_dir == None else output_dir / filename dest = filename if output_dir == None else output_dir / filename
copyfile(self.src, dest) copyfile(temp_file.name, dest)
old_ifc: ifcopenshell.file = ifcopenshell.open(dest) old_ifc: ifcopenshell.file = ifcopenshell.open(dest)
new_ifc = ifcopenshell.file(schema=self.file.schema) new_ifc = ifcopenshell.file(schema=self.file.schema)
@@ -98,9 +89,8 @@ class Patcher:
new_ifc.remove(element) new_ifc.remove(element)
new_ifc.write(dest) new_ifc.write(dest)
if temp_file is not None: temp_file.close()
temp_file.close() os.unlink(temp_file.name)
os.unlink(temp_file.name)
def is_in_storey(self, element: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance) -> bool: def is_in_storey(self, element: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance) -> bool:
return ( return (
@@ -30,7 +30,6 @@ from typing import Union
class Patcher: class Patcher:
def __init__( def __init__(
self, self,
src: str,
file: ifcopenshell.file, file: ifcopenshell.file,
logger: Logger, logger: Logger,
query: str = "IfcBeam", query: str = "IfcBeam",
@@ -58,7 +57,6 @@ class Patcher:
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "TessellateElements", "arguments": ["IfcBeam", False]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "TessellateElements", "arguments": ["IfcBeam", False]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.query = query self.query = query
+12 -9
View File
@@ -25,18 +25,22 @@ from logging import Logger
class Patcher: class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, query: str = ""): def __init__(self, file: ifcopenshell.file, logger: Logger, query: str = ""):
"""Create independent copies for shared psets in IFC file. """Create independent copies for shared psets in IFC file.
In IFC it's possible that same property set is shared by multiple elements, In IFC it's possible that same property set is shared by multiple
so editing it's properties will automatically change their values for all those elements. elements, so editing it's properties will automatically change their
values for all those elements.
Sometimes it's intended but sometimes it's not and it's just the way some other Sometimes it's intended but sometimes it's not and it's just the way
software exports IFC (e.g. there is a known case when Tekla exports shared psets for all the occurrences). some other software exports IFC (e.g. there is a known case when Tekla
While it is more optimized way to store data, it may lead to unexpected results when editing properties. exports shared psets for all the occurrences). While it is more
optimized way to store data, it may lead to unexpected results when
editing properties.
This recipe creates independent copies of all shared psets (may be limited by the query) This recipe creates independent copies of all shared psets (may be
and assigns them to the elements, so they can be edited without affecting any other elements. limited by the query) and assigns them to the elements, so they can be
edited without affecting any other elements.
:param query: A query to select the subset of IFC elements, optional. :param query: A query to select the subset of IFC elements, optional.
If not provided, patch will be applied to all shared property sets in the model. If not provided, patch will be applied to all shared property sets in the model.
@@ -51,7 +55,6 @@ class Patcher:
# Unshare psets on all IfcWalls. # Unshare psets on all IfcWalls.
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "UnsharePsets", "arguments": ["IfcWall"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "UnsharePsets", "arguments": ["IfcWall"]})
""" """
self.src = src
self.file = file self.file = file
self.logger = logger self.logger = logger
self.query = query self.query = query