Experimental undo and redo Python prototype (#1539)

* Experimental undo and redo Python prototype

* Simplify history, use transaction jargon, use walk for more robust serialisation

* Black file, set history size, add file reference to entity_instance constructor

* Ensure that files are always passed when entities are wrapped

* Add support for undo/redo of all project module operations

* Minor fix

* Blender to IFC mappings are now managed by Blender RNA, so they don't break on undo operations. See #1475.

* Implement undo for all attribute operations. See #1475.

* Update documentation for installation of add-on

* Revert Blender RNA approach for Blender-IFC mappings, because it didn't scale, but still fix the undo/redo object memory corruption with new "reload_linked_elements" method.
This commit is contained in:
Dion Moult
2021-06-30 18:34:12 +10:00
committed by GitHub
parent 10bf875e92
commit 6b0a58db7d
15 changed files with 609 additions and 112 deletions
@@ -110,6 +110,8 @@ if bpy is not None:
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.app.handlers.depsgraph_update_post.append(on_register) bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_post.append(handler.redo_post)
bpy.app.handlers.load_post.append(handler.setDefaultProperties) bpy.app.handlers.load_post.append(handler.setDefaultProperties)
bpy.app.handlers.load_post.append(handler.loadIfcStore) bpy.app.handlers.load_post.append(handler.loadIfcStore)
bpy.app.handlers.save_pre.append(handler.ensureIfcExported) bpy.app.handlers.save_pre.append(handler.ensureIfcExported)
+18 -6
View File
@@ -40,7 +40,7 @@ def name_callback(obj, data):
collection = obj.users_collection[0] collection = obj.users_collection[0]
collection.name = obj.name collection.name = obj.name
if element.is_a("IfcGrid"): if element.is_a("IfcGrid"):
axis_obj = IfcStore.id_map[element.UAxes[0].id()] axis_obj = IfcStore.get_element(element.UAxes[0].id())
axis_collection = axis_obj.users_collection[0] axis_collection = axis_obj.users_collection[0]
grid_collection = None grid_collection = None
for collection in bpy.data.collections: for collection in bpy.data.collections:
@@ -103,14 +103,26 @@ def loadIfcStore(scene):
if not ifc_file: if not ifc_file:
return return
IfcStore.get_schema() IfcStore.get_schema()
[ IfcStore.reload_linked_elements()
IfcStore.link_element(ifc_file.by_id(o.BIMObjectProperties.ifc_definition_id), o)
for o in bpy.data.objects
if o.BIMObjectProperties.ifc_definition_id
]
purge_module_data() purge_module_data()
@persistent
def undo_post(scene):
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.undo()
IfcStore.reload_linked_elements(should_reload_selected=True)
@persistent
def redo_post(scene):
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
IfcStore.redo()
IfcStore.reload_linked_elements(should_reload_selected=True)
@persistent @persistent
def ensureIfcExported(scene): def ensureIfcExported(scene):
if IfcStore.get_file() and not bpy.context.scene.BIMProperties.ifc_file: if IfcStore.get_file() and not bpy.context.scene.BIMProperties.ifc_file:
+59
View File
@@ -1,4 +1,5 @@
import bpy import bpy
import uuid
import ifcopenshell import ifcopenshell
import blenderbim.bim.handler import blenderbim.bim.handler
@@ -15,6 +16,9 @@ class IfcStore:
library_path = "" library_path = ""
library_file = None library_file = None
element_listeners = set() element_listeners = set()
last_transaction = ""
history = []
future = []
@staticmethod @staticmethod
def purge(): def purge():
@@ -65,6 +69,21 @@ class IfcStore:
def add_element_listener(callback): def add_element_listener(callback):
IfcStore.element_listeners.add(callback) IfcStore.element_listeners.add(callback)
@staticmethod
def reload_linked_elements(should_reload_selected=False):
file = IfcStore.get_file()
if not file:
return
if should_reload_selected:
objects = bpy.context.selected_objects + [bpy.context.active_object]
else:
objects = bpy.data.objects
[
IfcStore.link_element(file.by_id(obj.BIMObjectProperties.ifc_definition_id), obj)
for obj in objects
if obj.BIMObjectProperties.ifc_definition_id
]
@staticmethod @staticmethod
def link_element(element, obj): def link_element(element, obj):
IfcStore.id_map[element.id()] = obj IfcStore.id_map[element.id()] = obj
@@ -100,3 +119,43 @@ class IfcStore:
if obj: if obj:
obj.BIMObjectProperties.ifc_definition_id = 0 obj.BIMObjectProperties.ifc_definition_id = 0
@staticmethod
def generate_transaction_key(operator):
if not getattr(operator, "transaction_key", None):
setattr(operator, "transaction_key", str(uuid.uuid4()))
@staticmethod
def add_transaction(operator, rollback=None, commit=None):
IfcStore.generate_transaction_key(operator)
key = getattr(operator, "transaction_key", None)
data = getattr(operator, "transaction_data", None)
bpy.context.scene.BIMProperties.last_transaction = key
IfcStore.last_transaction = key
rollback = rollback or getattr(operator, "rollback", lambda data: True)
commit = commit or getattr(operator, "commit", lambda data: True)
if IfcStore.history and IfcStore.history[-1]["key"] == key:
IfcStore.history[-1]["transactions"].append({"rollback": rollback, "commit": commit, "data": data})
else:
IfcStore.history.append(
{"key": key, "transactions": [{"rollback": rollback, "commit": commit, "data": data}]}
)
IfcStore.future = []
@staticmethod
def undo():
if not IfcStore.history:
return
event = IfcStore.history.pop()
for transaction in event["transactions"][::-1]:
transaction["rollback"](transaction["data"])
IfcStore.future.append(event)
@staticmethod
def redo():
if not IfcStore.future:
return
event = IfcStore.future.pop()
for transaction in event["transactions"]:
transaction["commit"](transaction["data"])
IfcStore.history.append(event)
+1 -1
View File
@@ -704,7 +704,7 @@ class IfcImporter:
product = self.file.by_id(shape.guid) product = self.file.by_id(shape.guid)
# Facetation is to accommodate broken Revit files # Facetation is to accommodate broken Revit files
# See https://forums.buildingsmart.org/t/suggestions-on-how-to-improve-clarity-of-representation-context-usage-in-documentation/3663/6?u=moult # See https://forums.buildingsmart.org/t/suggestions-on-how-to-improve-clarity-of-representation-context-usage-in-documentation/3663/6?u=moult
if shape.context not in ["Body", "Facetation"] and shape.guid in IfcStore.guid_map: if shape.context not in ["Body", "Facetation"] and IfcStore.get_element(shape.guid):
# We only load a single context, and we prioritise the Body context. See #1290. # We only load a single context, and we prioritise the Body context. See #1290.
pass pass
elif product.is_a("IfcAnnotation") and product.ObjectType == "DRAWING": elif product.is_a("IfcAnnotation") and product.ObjectType == "DRAWING":
@@ -9,6 +9,7 @@ from ifcopenshell.api.attribute.data import Data
class EnableEditingAttributes(bpy.types.Operator): class EnableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.enable_editing_attributes" bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes" bl_label = "Enable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty() obj_type: bpy.props.StringProperty()
@@ -45,6 +46,7 @@ class EnableEditingAttributes(bpy.types.Operator):
class DisableEditingAttributes(bpy.types.Operator): class DisableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.disable_editing_attributes" bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes" bl_label = "Disable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty() obj_type: bpy.props.StringProperty()
@@ -61,6 +63,8 @@ class DisableEditingAttributes(bpy.types.Operator):
class EditAttributes(bpy.types.Operator): class EditAttributes(bpy.types.Operator):
bl_idname = "bim.edit_attributes" bl_idname = "bim.edit_attributes"
bl_label = "Edit Attributes" bl_label = "Edit Attributes"
bl_options = {"REGISTER", "UNDO"}
transaction_key: bpy.props.StringProperty()
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty() obj_type: bpy.props.StringProperty()
@@ -95,9 +99,11 @@ class EditAttributes(bpy.types.Operator):
elif attribute["type"] == "enum": elif attribute["type"] == "enum":
attributes[attribute["name"]] = blender_attribute.enum_value attributes[attribute["name"]] = blender_attribute.enum_value
product = self.file.by_id(oprops.ifc_definition_id) product = self.file.by_id(oprops.ifc_definition_id)
self.file.begin_transaction()
ifcopenshell.api.run( ifcopenshell.api.run(
"attribute.edit_attributes", self.file, **{"product": product, "attributes": attributes} "attribute.edit_attributes", self.file, **{"product": product, "attributes": attributes}
) )
self.file.end_transaction()
if "Name" in attributes: if "Name" in attributes:
new_name = "{}/{}".format(product.is_a(), product.Name or "Unnamed") new_name = "{}/{}".format(product.is_a(), product.Name or "Unnamed")
collection = bpy.data.collections.get(obj.name) collection = bpy.data.collections.get(obj.name)
@@ -106,12 +112,23 @@ class EditAttributes(bpy.types.Operator):
obj.name = new_name obj.name = new_name
Data.load(IfcStore.get_file(), oprops.ifc_definition_id) Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type) bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
self.transaction_data = {"ifc_definition_id": oprops.ifc_definition_id}
IfcStore.add_transaction(self)
return {"FINISHED"} return {"FINISHED"}
def rollback(self, data):
IfcStore.get_file().undo()
Data.load(IfcStore.get_file(), data["ifc_definition_id"])
def commit(self, data):
IfcStore.get_file().redo()
Data.load(IfcStore.get_file(), data["ifc_definition_id"])
class GenerateGlobalId(bpy.types.Operator): class GenerateGlobalId(bpy.types.Operator):
bl_idname = "bim.generate_global_id" bl_idname = "bim.generate_global_id"
bl_label = "Regenerate GlobalId" bl_label = "Regenerate GlobalId"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
index = bpy.context.active_object.BIMAttributeProperties.attributes.find("GlobalId") index = bpy.context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
@@ -2,6 +2,7 @@ import bpy
from . import ui, prop, operator from . import ui, prop, operator
classes = ( classes = (
operator.ValidateIfcFile,
operator.ProfileImportIFC, operator.ProfileImportIFC,
operator.CreateAllShapes, operator.CreateAllShapes,
operator.CreateShapeFromStepId, operator.CreateShapeFromStepId,
@@ -5,6 +5,19 @@ import blenderbim.bim.import_ifc as import_ifc
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
class ValidateIfcFile(bpy.types.Operator):
bl_idname = "bim.validate_ifc_file"
bl_label = "Validate IFC File"
def execute(self, context):
import ifcopenshell.validate
logger = logging.getLogger("validate")
logger.setLevel(logging.DEBUG)
ifcopenshell.validate.validate(IfcStore.get_file(), logger)
return {"FINISHED"}
class ProfileImportIFC(bpy.types.Operator): class ProfileImportIFC(bpy.types.Operator):
bl_idname = "bim.profile_import_ifc" bl_idname = "bim.profile_import_ifc"
bl_label = "Profile Import IFC" bl_label = "Profile Import IFC"
@@ -4,7 +4,6 @@ from . import ui, prop, operator
classes = ( classes = (
operator.CreateProject, operator.CreateProject,
operator.CreateProjectLibrary, operator.CreateProjectLibrary,
operator.ValidateIfcFile,
operator.SelectLibraryFile, operator.SelectLibraryFile,
operator.ChangeLibraryElement, operator.ChangeLibraryElement,
operator.RefreshLibrary, operator.RefreshLibrary,
@@ -2,6 +2,7 @@ import bpy
import logging import logging
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.representation
import bpy import bpy
import blenderbim.bim.handler import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
@@ -11,6 +12,8 @@ from blenderbim.bim import import_ifc
class CreateProject(bpy.types.Operator): class CreateProject(bpy.types.Operator):
bl_idname = "bim.create_project" bl_idname = "bim.create_project"
bl_label = "Create Project" bl_label = "Create Project"
bl_options = {"REGISTER", "UNDO"}
transaction_key: bpy.props.StringProperty()
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
@@ -22,6 +25,9 @@ class CreateProject(bpy.types.Operator):
) )
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
self.transaction_data = {"file": self.file}
IfcStore.add_transaction(self)
bpy.ops.bim.add_person() bpy.ops.bim.add_person()
bpy.ops.bim.add_organisation() bpy.ops.bim.add_organisation()
@@ -30,7 +36,7 @@ class CreateProject(bpy.types.Operator):
building = bpy.data.objects.new("My Building", None) building = bpy.data.objects.new("My Building", None)
building_storey = bpy.data.objects.new("Ground Floor", None) building_storey = bpy.data.objects.new("Ground Floor", None)
bpy.ops.bim.assign_class(obj=project.name, ifc_class="IfcProject") bpy.ops.bim.assign_class(transaction_key=self.transaction_key, obj=project.name, ifc_class="IfcProject")
bpy.ops.bim.assign_unit() bpy.ops.bim.assign_unit()
bpy.ops.bim.add_subcontext(context="Model") bpy.ops.bim.add_subcontext(context="Model")
bpy.ops.bim.add_subcontext(context="Model", subcontext="Body", target_view="MODEL_VIEW") bpy.ops.bim.add_subcontext(context="Model", subcontext="Body", target_view="MODEL_VIEW")
@@ -38,24 +44,35 @@ class CreateProject(bpy.types.Operator):
bpy.ops.bim.add_subcontext(context="Plan") bpy.ops.bim.add_subcontext(context="Plan")
bpy.ops.bim.add_subcontext(context="Plan", subcontext="Annotation", target_view="PLAN_VIEW") bpy.ops.bim.add_subcontext(context="Plan", subcontext="Annotation", target_view="PLAN_VIEW")
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"): bpy.context.scene.BIMProperties.contexts = str(
if subcontext.ContextIdentifier == "Body": ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW").id()
bpy.context.scene.BIMProperties.contexts = str(subcontext.id()) )
break
bpy.ops.bim.assign_class(obj=site.name, ifc_class="IfcSite") bpy.ops.bim.assign_class(transaction_key=self.transaction_key, obj=site.name, ifc_class="IfcSite")
bpy.ops.bim.assign_class(obj=building.name, ifc_class="IfcBuilding") bpy.ops.bim.assign_class(transaction_key=self.transaction_key, obj=building.name, ifc_class="IfcBuilding")
bpy.ops.bim.assign_class(obj=building_storey.name, ifc_class="IfcBuildingStorey") bpy.ops.bim.assign_class(
transaction_key=self.transaction_key, obj=building_storey.name, ifc_class="IfcBuildingStorey"
)
bpy.ops.bim.assign_object(related_object=site.name, relating_object=project.name) bpy.ops.bim.assign_object(related_object=site.name, relating_object=project.name)
bpy.ops.bim.assign_object(related_object=building.name, relating_object=site.name) bpy.ops.bim.assign_object(related_object=building.name, relating_object=site.name)
bpy.ops.bim.assign_object(related_object=building_storey.name, relating_object=building.name) bpy.ops.bim.assign_object(related_object=building_storey.name, relating_object=building.name)
# Data.load()
return {"FINISHED"} return {"FINISHED"}
def rollback(self, data):
IfcStore.file = None
blenderbim.bim.handler.purge_module_data()
def commit(self, data):
blenderbim.bim.handler.purge_module_data()
IfcStore.file = data["file"]
class CreateProjectLibrary(bpy.types.Operator): class CreateProjectLibrary(bpy.types.Operator):
bl_idname = "bim.create_project_library" bl_idname = "bim.create_project_library"
bl_label = "Create Project Library" bl_label = "Create Project Library"
bl_options = {"REGISTER", "UNDO"}
transaction_key: bpy.props.StringProperty()
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
@@ -67,45 +84,62 @@ class CreateProjectLibrary(bpy.types.Operator):
) )
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
self.transaction_data = {"file": self.file}
IfcStore.add_transaction(self)
if self.file.schema == "IFC2X3": if self.file.schema == "IFC2X3":
bpy.ops.bim.add_person() bpy.ops.bim.add_person()
bpy.ops.bim.add_organisation() bpy.ops.bim.add_organisation()
project_library = bpy.data.objects.new("My Project Library", None) project_library = bpy.data.objects.new("My Project Library", None)
bpy.ops.bim.assign_class(obj=project_library.name, ifc_class="IfcProjectLibrary") bpy.ops.bim.assign_class(
transaction_key=self.transaction_key, obj=project_library.name, ifc_class="IfcProjectLibrary"
)
bpy.ops.bim.assign_unit() bpy.ops.bim.assign_unit()
return {"FINISHED"} return {"FINISHED"}
def rollback(self, data):
IfcStore.file = None
blenderbim.bim.handler.purge_module_data()
class ValidateIfcFile(bpy.types.Operator): def commit(self, data):
bl_idname = "bim.validate_ifc_file" blenderbim.bim.handler.purge_module_data()
bl_label = "Validate IFC File" IfcStore.file = data["file"]
def execute(self, context):
import ifcopenshell.validate
logger = logging.getLogger("validate")
logger.setLevel(logging.DEBUG)
ifcopenshell.validate.validate(IfcStore.get_file(), logger)
return {"FINISHED"}
class SelectLibraryFile(bpy.types.Operator): class SelectLibraryFile(bpy.types.Operator):
bl_idname = "bim.select_library_file" bl_idname = "bim.select_library_file"
bl_label = "Select Library File" bl_label = "Select Library File"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH") filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
def execute(self, context): def execute(self, context):
old_filepath = IfcStore.library_path
IfcStore.library_path = self.filepath IfcStore.library_path = self.filepath
IfcStore.library_file = ifcopenshell.open(self.filepath) IfcStore.library_file = ifcopenshell.open(self.filepath)
bpy.ops.bim.refresh_library() bpy.ops.bim.refresh_library()
self.transaction_data = {"old_filepath": old_filepath, "filepath": self.filepath}
IfcStore.add_transaction(self)
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
def rollback(self, data):
if data["old_filepath"]:
IfcStore.library_path = data["old_filepath"]
IfcStore.library_file = ifcopenshell.open(data["old_filepath"])
else:
IfcStore.library_path = ""
IfcStore.library_file = None
def commit(self, data):
IfcStore.library_path = data["filepath"]
IfcStore.library_file = ifcopenshell.open(data["filepath"])
class RefreshLibrary(bpy.types.Operator): class RefreshLibrary(bpy.types.Operator):
bl_idname = "bim.refresh_library" bl_idname = "bim.refresh_library"
@@ -132,6 +166,7 @@ class RefreshLibrary(bpy.types.Operator):
class ChangeLibraryElement(bpy.types.Operator): class ChangeLibraryElement(bpy.types.Operator):
bl_idname = "bim.change_library_element" bl_idname = "bim.change_library_element"
bl_label = "Change Library Element" bl_label = "Change Library Element"
bl_options = {"REGISTER", "UNDO"}
element_name: bpy.props.StringProperty() element_name: bpy.props.StringProperty()
def execute(self, context): def execute(self, context):
@@ -163,6 +198,7 @@ class ChangeLibraryElement(bpy.types.Operator):
class RewindLibrary(bpy.types.Operator): class RewindLibrary(bpy.types.Operator):
bl_idname = "bim.rewind_library" bl_idname = "bim.rewind_library"
bl_label = "Rewind Library" bl_label = "Rewind Library"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
self.props = context.scene.BIMProjectProperties self.props = context.scene.BIMProjectProperties
@@ -180,40 +216,62 @@ class RewindLibrary(bpy.types.Operator):
class AssignLibraryDeclaration(bpy.types.Operator): class AssignLibraryDeclaration(bpy.types.Operator):
bl_idname = "bim.assign_library_declaration" bl_idname = "bim.assign_library_declaration"
bl_label = "Assign Library Declaration" bl_label = "Assign Library Declaration"
bl_options = {"REGISTER", "UNDO"}
definition: bpy.props.IntProperty() definition: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
self.props = context.scene.BIMProjectProperties self.props = context.scene.BIMProjectProperties
self.file = IfcStore.library_file
self.file.begin_transaction()
ifcopenshell.api.run( ifcopenshell.api.run(
"project.assign_declaration", "project.assign_declaration",
IfcStore.library_file, self.file,
definition=IfcStore.library_file.by_id(self.definition), definition=self.file.by_id(self.definition),
relating_context=IfcStore.library_file.by_type("IfcProjectLibrary")[0], relating_context=self.file.by_type("IfcProjectLibrary")[0],
) )
self.file.end_transaction()
element_name = self.props.active_library_element element_name = self.props.active_library_element
bpy.ops.bim.rewind_library() bpy.ops.bim.rewind_library()
bpy.ops.bim.change_library_element(element_name = element_name) bpy.ops.bim.change_library_element(element_name=element_name)
IfcStore.add_transaction(self)
return {"FINISHED"} return {"FINISHED"}
def rollback(self, data):
IfcStore.library_file.undo()
def commit(self, data):
IfcStore.library_file.redo()
class UnassignLibraryDeclaration(bpy.types.Operator): class UnassignLibraryDeclaration(bpy.types.Operator):
bl_idname = "bim.unassign_library_declaration" bl_idname = "bim.unassign_library_declaration"
bl_label = "Unassign Library Declaration" bl_label = "Unassign Library Declaration"
bl_options = {"REGISTER", "UNDO"}
definition: bpy.props.IntProperty() definition: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
self.props = context.scene.BIMProjectProperties self.props = context.scene.BIMProjectProperties
self.file = IfcStore.library_file
self.file.begin_transaction()
ifcopenshell.api.run( ifcopenshell.api.run(
"project.unassign_declaration", "project.unassign_declaration",
IfcStore.library_file, self.file,
definition=IfcStore.library_file.by_id(self.definition), definition=self.file.by_id(self.definition),
relating_context=IfcStore.library_file.by_type("IfcProjectLibrary")[0], relating_context=self.file.by_type("IfcProjectLibrary")[0],
) )
self.file.end_transaction()
element_name = self.props.active_library_element element_name = self.props.active_library_element
bpy.ops.bim.rewind_library() bpy.ops.bim.rewind_library()
bpy.ops.bim.change_library_element(element_name = element_name) bpy.ops.bim.change_library_element(element_name=element_name)
IfcStore.add_transaction(self)
return {"FINISHED"} return {"FINISHED"}
def rollback(self, data):
IfcStore.library_file.undo()
def commit(self, data):
IfcStore.library_file.redo()
class SaveLibraryFile(bpy.types.Operator): class SaveLibraryFile(bpy.types.Operator):
bl_idname = "bim.save_library_file" bl_idname = "bim.save_library_file"
@@ -227,17 +285,22 @@ class SaveLibraryFile(bpy.types.Operator):
class AppendLibraryElement(bpy.types.Operator): class AppendLibraryElement(bpy.types.Operator):
bl_idname = "bim.append_library_element" bl_idname = "bim.append_library_element"
bl_label = "Append Library Element" bl_label = "Append Library Element"
bl_options = {"REGISTER", "UNDO"}
definition: bpy.props.IntProperty() definition: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file()
self.file.begin_transaction()
element = ifcopenshell.api.run( element = ifcopenshell.api.run(
"project.append_asset", "project.append_asset",
IfcStore.get_file(), self.file,
library=IfcStore.library_file, library=IfcStore.library_file,
element=IfcStore.library_file.by_id(self.definition), element=IfcStore.library_file.by_id(self.definition),
) )
self.file.end_transaction()
self.import_type_from_ifc(element) self.import_type_from_ifc(element)
blenderbim.bim.handler.purge_module_data() blenderbim.bim.handler.purge_module_data()
IfcStore.add_transaction(self)
return {"FINISHED"} return {"FINISHED"}
def import_type_from_ifc(self, element): def import_type_from_ifc(self, element):
@@ -259,10 +322,17 @@ class AppendLibraryElement(bpy.types.Operator):
ifc_importer.create_type_product(element) ifc_importer.create_type_product(element)
ifc_importer.place_objects_in_spatial_tree() ifc_importer.place_objects_in_spatial_tree()
def rollback(self, data):
IfcStore.get_file().undo()
def commit(self, data):
IfcStore.get_file().redo()
class EnableEditingHeader(bpy.types.Operator): class EnableEditingHeader(bpy.types.Operator):
bl_idname = "bim.enable_editing_header" bl_idname = "bim.enable_editing_header"
bl_label = "Enable Editing Header" bl_label = "Enable Editing Header"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
@@ -294,23 +364,53 @@ class EnableEditingHeader(bpy.types.Operator):
class EditHeader(bpy.types.Operator): class EditHeader(bpy.types.Operator):
bl_idname = "bim.edit_header" bl_idname = "bim.edit_header"
bl_label = "Edit Header" bl_label = "Edit Header"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
props = context.scene.BIMProjectProperties props = context.scene.BIMProjectProperties
props.is_editing = True props.is_editing = True
self.file.wrapped_data.header.file_description.description = (f'ViewDefinition[{props.mvd}]',) self.transaction_data = {}
self.transaction_data["old"] = self.record_state()
self.file.wrapped_data.header.file_description.description = (f"ViewDefinition[{props.mvd}]",)
self.file.wrapped_data.header.file_name.author = (props.author_name, props.author_email) self.file.wrapped_data.header.file_name.author = (props.author_name, props.author_email)
self.file.wrapped_data.header.file_name.organization = (props.organisation_name, props.organisation_email) self.file.wrapped_data.header.file_name.organization = (props.organisation_name, props.organisation_email)
self.file.wrapped_data.header.file_name.authorization = props.authorisation self.file.wrapped_data.header.file_name.authorization = props.authorisation
bpy.ops.bim.disable_editing_header() bpy.ops.bim.disable_editing_header()
self.transaction_data["new"] = self.record_state()
IfcStore.add_transaction(self)
return {"FINISHED"} return {"FINISHED"}
def record_state(self):
return {
"description": self.file.wrapped_data.header.file_description.description,
"author": self.file.wrapped_data.header.file_name.author,
"organisation": self.file.wrapped_data.header.file_name.organization,
"authorisation": self.file.wrapped_data.header.file_name.authorization,
}
def rollback(self, data):
file = IfcStore.get_file()
file.wrapped_data.header.file_description.description = data["old"]["description"]
file.wrapped_data.header.file_name.author = data["old"]["author"]
file.wrapped_data.header.file_name.organization = data["old"]["organisation"]
file.wrapped_data.header.file_name.authorization = data["old"]["authorisation"]
def commit(self, data):
file = IfcStore.get_file()
file.wrapped_data.header.file_description.description = data["new"]["description"]
file.wrapped_data.header.file_name.author = data["new"]["author"]
file.wrapped_data.header.file_name.organization = data["new"]["organisation"]
file.wrapped_data.header.file_name.authorization = data["new"]["authorisation"]
class DisableEditingHeader(bpy.types.Operator): class DisableEditingHeader(bpy.types.Operator):
bl_idname = "bim.disable_editing_header" bl_idname = "bim.disable_editing_header"
bl_label = "Disable Editing Header" bl_label = "Disable Editing Header"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
context.scene.BIMProjectProperties.is_editing = False context.scene.BIMProjectProperties.is_editing = False
@@ -78,6 +78,7 @@ class AssignClass(bpy.types.Operator):
bl_idname = "bim.assign_class" bl_idname = "bim.assign_class"
bl_label = "Assign IFC Class" bl_label = "Assign IFC Class"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
transaction_key: bpy.props.StringProperty()
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty() ifc_class: bpy.props.StringProperty()
predefined_type: bpy.props.StringProperty() predefined_type: bpy.props.StringProperty()
@@ -87,6 +88,7 @@ class AssignClass(bpy.types.Operator):
ifc_representation_class: bpy.props.StringProperty() ifc_representation_class: bpy.props.StringProperty()
def execute(self, context): def execute(self, context):
self.transaction_data = []
objects = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects objects = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
self.declaration = IfcStore.get_schema().declaration_by_name(self.ifc_class) self.declaration = IfcStore.get_schema().declaration_by_name(self.ifc_class)
@@ -96,11 +98,13 @@ class AssignClass(bpy.types.Operator):
predefined_type = None predefined_type = None
for obj in objects: for obj in objects:
self.assign_class(context, obj) self.assign_class(context, obj)
IfcStore.add_transaction(self)
return {"FINISHED"} return {"FINISHED"}
def assign_class(self, context, obj): def assign_class(self, context, obj):
if obj.BIMObjectProperties.ifc_definition_id: if obj.BIMObjectProperties.ifc_definition_id:
return return
self.file.begin_transaction()
product = ifcopenshell.api.run( product = ifcopenshell.api.run(
"root.create_entity", "root.create_entity",
self.file, self.file,
@@ -110,8 +114,10 @@ class AssignClass(bpy.types.Operator):
"name": obj.name, "name": obj.name,
}, },
) )
self.file.end_transaction()
obj.name = "{}/{}".format(product.is_a(), obj.name) obj.name = "{}/{}".format(product.is_a(), obj.name)
IfcStore.link_element(product, obj) IfcStore.link_element(product, obj)
self.transaction_data.append({"element": product.id(), "obj": obj.name})
if self.should_add_representation: if self.should_add_representation:
bpy.ops.bim.add_representation( bpy.ops.bim.add_representation(
@@ -171,6 +177,20 @@ class AssignClass(bpy.types.Operator):
) )
break break
def rollback(self, data):
for linked_element in data:
IfcStore.unlink_element(
IfcStore.get_file().by_id(linked_element["element"]), bpy.data.objects.get(linked_element["obj"])
)
IfcStore.get_file().undo()
def commit(self, data):
IfcStore.get_file().redo()
for linked_element in data:
IfcStore.link_element(
IfcStore.get_file().by_id(linked_element["element"]), bpy.data.objects.get(linked_element["obj"])
)
class UnassignClass(bpy.types.Operator): class UnassignClass(bpy.types.Operator):
bl_idname = "bim.unassign_class" bl_idname = "bim.unassign_class"
@@ -301,7 +301,7 @@ class BIMWorkScheduleProperties(PropertyGroup):
class BIMTaskTreeProperties(PropertyGroup): class BIMTaskTreeProperties(PropertyGroup):
# This belongs by itself for performance reasons. https://developer.blender.org/T87737 # This belongs by itself for performance reasons. https://developer.blender.org/T87737
# In Blender if you add thousands of tasks it makes other property access in the same group really slow. # In Blender if you add many collection items it makes other property access in the same group really slow.
tasks: CollectionProperty(name="Tasks", type=Task) tasks: CollectionProperty(name="Tasks", type=Task)
+1 -2
View File
@@ -174,9 +174,8 @@ class BIMProperties(PropertyGroup):
default=os.path.join(cwd, "data") + os.path.sep, name="Data Directory", update=updateDataDir default=os.path.join(cwd, "data") + os.path.sep, name="Data Directory", update=updateDataDir
) )
ifc_file: StringProperty(name="IFC File", update=updateIfcFile) ifc_file: StringProperty(name="IFC File", update=updateIfcFile)
id_map: StringProperty(name="ID Map")
guid_map: StringProperty(name="GUID Map")
export_schema: EnumProperty(items=[("IFC4", "IFC4", ""), ("IFC2X3", "IFC2X3", "")], name="IFC Schema") export_schema: EnumProperty(items=[("IFC4", "IFC4", ""), ("IFC2X3", "IFC2X3", "")], name="IFC Schema")
last_transaction: StringProperty(name="Last Transaction")
contexts: EnumProperty(items=getContexts, name="Contexts") contexts: EnumProperty(items=getContexts, name="Contexts")
available_contexts: EnumProperty(items=[("Model", "Model", ""), ("Plan", "Plan", "")], name="Available Contexts") available_contexts: EnumProperty(items=[("Model", "Model", ""), ("Plan", "Plan", "")], name="Available Contexts")
available_subcontexts: EnumProperty(items=getSubcontexts, name="Available Subcontexts") available_subcontexts: EnumProperty(items=getSubcontexts, name="Available Subcontexts")
+178 -50
View File
@@ -1,91 +1,219 @@
Installation Installation
============ ============
BlenderBIM is packaged as a Blender add-on, so installation is the same as any There are different methods of installation, depending on your situation.
other Blender add-on. The full instructions for end-user installation is
available at the `Get BlenderBIM <https://blenderbim.org/download.html>`_ 1. **Packaged installation** is recommended for regular users.
website. 2. **Daily build installation** is recommended for power users helping with testing.
3. **Unpackaged installation** is recommended for package managers.
4. **Source installation** is recommended for developers.
Packaged installation
---------------------
The BlenderBIM Add-on is packaged like a regular Blender add-on, so installation
is the same as any other Blender add-on. The full instructions for end-user
installation is available at the `Get BlenderBIM
<https://blenderbim.org/download.html>`__ website. The latest release is
typically updated every few weeks.
If you downloaded Blender as a ``.zip`` file without running an installer, you If you downloaded Blender as a ``.zip`` file without running an installer, you
will find the BlenderBIM plug-in installed in: will find the BlenderBIM plug-in installed in the following directory, where
``2.XX`` is the Blender version:
:: ::
/path/to/blender/2.81/scripts/addons/ /path/to/blender/2.XX/scripts/addons/
Otherwise, if you installed Blender using an installation package, the add-ons Otherwise, if you installed Blender using an installation package, the add-ons
folder depends on which operating system you use. On Linux: folder depends on which operating system you use. On Linux:
:: ::
~/.config/blender/2.81/scripts/addons/ ~/.config/blender/2.XX/scripts/addons/
On Mac: On Mac:
:: ::
/Users/{YOUR_USER}/Library/Application Support/Blender/2.81/ /Users/{YOUR_USER}/Library/Application Support/Blender/2.XX/
On Windows: On Windows:
:: ::
C:\Users\{YOUR_USER}\AppData\Roaming\Blender Foundation\2.81\scripts\addons C:\Users\{YOUR_USER}\AppData\Roaming\Blender Foundation\2.XX\scripts\addons
Upon installation, a series of files will be created. This is necessary as Upon installation, the BlenderBIM Add-on is stored in the ``blenderbim/``
BlenderBIM has a variety of complex dependencies. A full list is below: directory.
Daily build installation
------------------------
Daily builds are almost the same as **Packaged installation**, except that they
are typically updated every day. Simply download a daily build from the `Github
releases page <https://github.com/IfcOpenShell/IfcOpenShell/releases>`__, then
follow the same instructions as a packaged installation.
You will need to choose which daily build to download.
- If you are on Blender <2.93, choose py37
- If you are on Blender >=2.93, choose py39
- Choose linux, macos, or win depending on your operating system
Daily builds are not always stable. Sometimes, a build may be delayed, or
contain broken code. We try to avoid this, but it happens.
Unpackaged installation
-----------------------
The BlenderBIM Add-on is fully contained in the ``blenderbim/`` subfolder of the
Blender add-ons directory. This is typically distributed as a zipfile as per
Blender add-on conventions. Within this folder, you'll find the following file
structure:
:: ::
blenderbim/ bim/ (core code)
ifcopenshell/ libs/ (dependencies)
OCC/ __init__.py
pystache/
svgwrite/
deepdiff/
jsonpickle/
lib/ # Note: this only exists on MacOS and Linux
ordered_set.py
pyparsing.py
If you are not on Windows, when BlenderBIM first launches, it will create a This corresponds to the structure found in the source code `here
bunch of library files in the ``2.81/`` folder too. This is a non-standard <https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.6.0/src/blenderbim/blenderbim>`__.
location to place files, but is a hack to allow people to use precompiled builds
from Conda. The BlenderBIM Add-on is complex, and requires many dependencies, including
Python modules, binaries, and static assets. These dependencies are bundled with
the add-on for convenience in the **Packaged installation** and **Daily build
installation** methods.
If you choose to install the BlenderBIM Add-on and use your own system
dependencies, the source of truth for how dependencies are bundled are found in
the `Makefile
<https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.6.0/src/blenderbim/Makefile>`__.
Required Python modules to be stored in ``libs/site/packages/`` are:
::
ifcopenshell
bcf
ifcclash
bimtester
ifccobie
ifcdiff
ifccsv
ifcpatch
ifcp6
pystache
svgwrite
dateutil
isodate
networkx
deepdiff
jsonpickle
ordered_set
pyparsing
xmlschema
elementpath
six
lark-parser
fcl
behave
parse
parse_type
xlsxwriter
odfpy
defusedxml
boto3
botocore
jmespath
s3transfer
ifcjson
Notes:
1. ``ifcopenshell`` almost always requires the latest version due to the fast paced nature of the add-on development.
2. ``fcl`` is not bundled for MacOS, due to lack of maintained community build. This is required for clash detection.
3. ``behave`` requires `patches <https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.6.0/src/ifcbimtester/patch>`__.
4. ``ifcjson`` can be found `here <https://github.com/IFCJSON-Team/IFC2JSON_python/tree/master/file_converters>`__.
Required binaries are:
::
libs/IfcConvert
Required static assets are:
::
bim/data/gantt/jsgantt.js (from jsgantt-improved)
bim/data/gantt/jsgantt.css (from jsgantt-improved)
If you receive an error when enabling the add-on, you may have installed the If you receive an error when enabling the add-on, you may have installed the
package for the wrong platform. package for the wrong platform.
Updating Source installation
-------- -------------------
It is recommended to uninstall the current BlenderBIM add-on before installing
the latest version to ensure the update goes well.
From Source
-----------
It is possible to run the latest bleeding edge version of BlenderBIM without It is possible to run the latest bleeding edge version of BlenderBIM without
having to wait for an official release, since BlenderBIM is coded in Python and having to wait for an official release, since BlenderBIM is coded in Python and
doesn't require any compilation. First, install the latest official release, and doesn't require any compilation.
then `download the latest source code
<https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.6.0.zip>`_. If you know You can create your own package by using the Makefile as shown below. You can
how to use Git, you can also stay up to date like so: choose between a ``PLATFORM`` of ``linux``, ``macos``, and ``win``. You can
choose between a ``PYVERSION`` of ``py39`` and ``py37``.
::
$ cd src/blenderbim
$ make dist PLATFORM=linux PYVERSION=py39
$ ls dist/
However, creating a build, uninstalling the old add-on, and installing a new
build is a slow process. A more rapid approach is to follow the **Daily build
installation** method, as this provides all dependencies for you out of the box.
Then, we can replace certain Python files that tend to be updated frequently
with those from the Git repository. We're going to use symlinks (Windows user
can use ``mklink``), so we can code in our Git repository, and see the changes
in our Blender installation.
In addition, we're also going to replace the Python code of the IfcOpenShell
dependency with our Git repository, since most of the BlenderBIM Add-on
functionality is agnostic of Blender, and is actually part of IfcOpenShell.
Therefore, we need to keep this dependency highly updated as well.
The downside with this approach is that if a new dependency is added, or a
compiled dependency version requirement has changed, or the build system
changes, you'll need to fix your setup manually. But this is relatively rare.
:: ::
$ git clone https://github.com/IfcOpenShell/IfcOpenShell.git $ git clone https://github.com/IfcOpenShell/IfcOpenShell.git
$ cd IfcOpenShell
$ git checkout v0.6.0
Then, just copy the files from the source code's # Remove the Blender add-on Python code
``src/blenderbim/blenderbim/`` folder and replace the files in your $ rm -r /path/to/blender/2.XX/scripts/addons/blenderbim/bim/
Blender add-on's ``blenderbim/`` folder.
Restart Blender for the changes to take effect. In ``Edit > Preferences > # Replace them with links to the Git repository
Add-ons`` you will see that the version number of BlenderBIM has changed to $ ln -s src/blenderbim/blenderbim/bim /path/to/blender/2.XX/scripts/addons/blenderbim/bim
``0.0.999999``, which represents an un-versioned BlenderBIM.
# Remove the IfcOpenShell dependency Python code
$ rm -r /path/to/blender/2.XX/scripts/addons/blenderbim/libs/site/packages/ifcopenshell/api
$ rm -r /path/to/blender/2.XX/scripts/addons/blenderbim/libs/site/packages/ifcopenshell/util
# Replace them with links to the Git repository
$ ln -s src/ifcopenshell-python/ifcopenshell/api /path/to/blender/2.XX/scripts/addons/blenderbim/libs/site/packages/ifcopenshell/api
$ ln -s src/ifcopenshell-python/ifcopenshell/util /path/to/blender/2.XX/scripts/addons/blenderbim/libs/site/packages/ifcopenshell/util
After you modify your code in the Git repository, you will need to restart
Blender for the changes to take effect. In ``Edit > Preferences > Add-ons`` you
will see that the version number of BlenderBIM has changed to ``0.0.999999``,
which represents an un-versioned BlenderBIM.
Updating
--------
First uninstall the current BlenderBIM add-on, then install the latest version.
Uninstalling Uninstalling
------------ ------------
You can remove all of the files added by BlenderBIM in the Blender add-ons Navigate to ``Edit > Preferences > Add-ons``. Due to a limitation in Blender,
folder and then remove the add-on using the Blender interface through ``Edit > you have to first disable the BlenderBIM Add-on in your Blender preferences by
Preferences > Add-ons`` just like any other add-on. pressing the checkbox next to the add-on, then restart Blender. After
restarting, you can uninstall the BlenderBIM Add-on by pressing the ``Remove``
button in the Blender preferences window.
If you are not on Windows, then ensure that all library files are deleted in the Alternatively, you may uninstall manually by deleting the ``blenderbim/``
``2.81/`` directory. Do not delete any of Blender's own folders. directory in your Blender add-ons directory.
@@ -48,20 +48,21 @@ class entity_instance(object):
>>> #423=IfcProductDefinitionShape($,$,(#409,#421)) >>> #423=IfcProductDefinitionShape($,$,(#409,#421))
""" """
def __init__(self, e): def __init__(self, e, file):
if isinstance(e, tuple): if isinstance(e, tuple):
e = ifcopenshell_wrapper.new_IfcBaseClass(*e) e = ifcopenshell_wrapper.new_IfcBaseClass(*e)
super(entity_instance, self).__setattr__("wrapped_data", e) super(entity_instance, self).__setattr__("wrapped_data", e)
self.wrapped_data.file = file
def __getattr__(self, name): def __getattr__(self, name):
INVALID, FORWARD, INVERSE = range(3) INVALID, FORWARD, INVERSE = range(3)
attr_cat = self.wrapped_data.get_attribute_category(name) attr_cat = self.wrapped_data.get_attribute_category(name)
if attr_cat == FORWARD: if attr_cat == FORWARD:
return entity_instance.wrap_value( return entity_instance.wrap_value(
self.wrapped_data.get_argument(self.wrapped_data.get_argument_index(name)) self.wrapped_data.get_argument(self.wrapped_data.get_argument_index(name)), self.wrapped_data.file
) )
elif attr_cat == INVERSE: elif attr_cat == INVERSE:
return entity_instance.wrap_value(self.wrapped_data.get_inverse(name)) return entity_instance.wrap_value(self.wrapped_data.get_inverse(name), self.wrapped_data.file)
else: else:
raise AttributeError( raise AttributeError(
"entity instance of type '%s' has no attribute '%s'" % (self.wrapped_data.is_a(), name) "entity instance of type '%s' has no attribute '%s'" % (self.wrapped_data.is_a(), name)
@@ -77,9 +78,9 @@ class entity_instance(object):
return value return value
@staticmethod @staticmethod
def wrap_value(v): def wrap_value(v, file):
def wrap(e): def wrap(e):
return entity_instance(e) return entity_instance(e, file)
def is_instance(e): def is_instance(e):
return isinstance(e, ifcopenshell_wrapper.entity_instance) return isinstance(e, ifcopenshell_wrapper.entity_instance)
@@ -116,12 +117,15 @@ class entity_instance(object):
return self.wrapped_data.get_argument_name(attr_idx) return self.wrapped_data.get_argument_name(attr_idx)
def __setattr__(self, key, value): def __setattr__(self, key, value):
self[self.wrapped_data.get_argument_index(key)] = value index = self.wrapped_data.get_argument_index(key)
if self.wrapped_data.file.transaction:
self.wrapped_data.file.transaction.store_edit(self, index, value)
self[index] = value
def __getitem__(self, key): def __getitem__(self, key):
if key < 0 or key >= len(self): if key < 0 or key >= len(self):
raise IndexError("Attribute index {} out of range for instance of type {}".format(key, self.is_a())) raise IndexError("Attribute index {} out of range for instance of type {}".format(key, self.is_a()))
return entity_instance.wrap_value(self.wrapped_data.get_argument(key)) return entity_instance.wrap_value(self.wrapped_data.get_argument(key), self.wrapped_data.file)
def __setitem__(self, idx, value): def __setitem__(self, idx, value):
attr_type = real_attr_type = self.attribute_type(idx).title().replace(" ", "") attr_type = real_attr_type = self.attribute_type(idx).title().replace(" ", "")
@@ -271,7 +275,7 @@ class entity_instance(object):
return return_type(_()) return return_type(_())
__dict__ = property(get_info) __dict__ = property(get_info)
def get_info_2(self, include_identifier=True, recursive=False, return_type=dict, ignore=()): def get_info_2(self, include_identifier=True, recursive=False, return_type=dict, ignore=()):
assert include_identifier assert include_identifier
assert recursive assert recursive
+155 -12
View File
@@ -35,6 +35,105 @@ except NameError:
basestring = (str, bytes) basestring = (str, bytes)
class Transaction:
def __init__(self, ifc_file):
self.file = ifc_file
self.operations = []
def serialise_entity_instance(self, element):
info = element.get_info()
for key, value in info.items():
info[key] = self.serialise_value(element, value)
return info
def serialise_value(self, element, value):
return element.walk(lambda v: isinstance(v, entity_instance), lambda v: {"id": v.id()}, value)
def unserialise_value(self, element, value):
return element.walk(lambda v: isinstance(v, dict), lambda v: self.file.by_id(v["id"]), value)
def store_create(self, element):
self.operations.append({"action": "create", "value": self.serialise_entity_instance(element)})
def store_edit(self, element, index, value):
self.operations.append(
{
"action": "edit",
"id": element.id(),
"index": index,
"old": self.serialise_value(element, element[index]),
"new": self.serialise_value(element, value),
}
)
def store_delete(self, element):
inverses = {}
for inverse in self.file.get_inverse(element):
inverse_references = []
for i, attribute in enumerate(inverse):
if attribute == element:
inverse_references.append((i, "single"))
elif isinstance(attribute, tuple) and element in attribute:
inverse_references.append((i, "multiple"))
inverses[inverse.id()] = inverse_references
self.operations.append(
{"action": "delete", "inverses": inverses, "value": self.serialise_entity_instance(element)}
)
def rollback(self):
for operation in self.operations[::-1]:
if operation["action"] == "create":
element = self.file.by_id(operation["value"]["id"])
if hasattr(element, "GlobalId"):
# hack, otherwise ifcopenshell gets upset
element.GlobalId = "x"
self.file.remove(element)
elif operation["action"] == "edit":
element = self.file.by_id(operation["id"])
try:
element[operation["index"]] = self.unserialise_value(element, operation["old"])
except:
# Catch discrepancy where IfcOpenShell creates but doesn't allow editing of invalid values
pass
elif operation["action"] == "delete":
e = self.file.create_entity(operation["value"]["type"], id=operation["value"]["id"])
for k, v in operation["value"].items():
try:
setattr(e, k, self.unserialise_value(e, v))
except:
# Catch discrepancy where IfcOpenShell creates but doesn't allow editing of invalid values
pass
for inverse_id, data in operation["inverses"].items():
inverse = self.file.by_id(inverse_id)
for index, data_type in data:
if data_type == "single":
inverse[index] = e
elif data_type == "multiple":
if inverse[index] is None:
inverse[index] = e
else:
new = list(inverse[index])
new.append(e)
inverse[index] = new
def commit(self):
for operation in self.operations:
if operation["action"] == "create":
e = self.file.create_entity(operation["value"]["type"], id=operation["value"]["id"])
for k, v in operation["value"].items():
try:
setattr(e, k, self.unserialise_value(e, v))
except:
# Catch discrepancy where IfcOpenShell creates but doesn't allow editing of invalid values
pass
elif operation["action"] == "edit":
element = self.file.by_id(operation["id"])
element[operation["index"]] = self.unserialise_value(element, operation["new"])
elif operation["action"] == "delete":
element = self.file.by_id(operation["value"]["id"])
self.file.remove(element)
class file(object): class file(object):
"""Base class for containing IFC files. """Base class for containing IFC files.
@@ -59,6 +158,45 @@ class file(object):
args = filter(None, [schema]) args = filter(None, [schema])
args = map(ifcopenshell_wrapper.schema_by_name, args) args = map(ifcopenshell_wrapper.schema_by_name, args)
self.wrapped_data = ifcopenshell_wrapper.file(*args) self.wrapped_data = ifcopenshell_wrapper.file(*args)
self.history_size = 64
self.history = []
self.future = []
self.transaction = None
def set_history_size(self, size):
self.history_size = size
while len(self.history) > self.history_size:
self.history.pop(0)
def begin_transaction(self):
self.transaction = Transaction(self)
def end_transaction(self):
if self.transaction:
self.history.append(self.transaction)
if len(self.history) > self.history_size:
self.history.pop(0)
self.future = []
self.transaction = None
def discard_transaction(self):
if self.transaction:
self.transaction.rollback()
self.transaction = None
def undo(self):
if not self.history:
return
transaction = self.history.pop()
transaction.rollback()
self.future.append(transaction)
def redo(self):
if not self.future:
return
transaction = self.future.pop()
transaction.commit()
self.history.append(transaction)
def create_entity(self, type, *args, **kwargs): def create_entity(self, type, *args, **kwargs):
"""Create a new IFC entity in the file. """Create a new IFC entity in the file.
@@ -82,14 +220,17 @@ class file(object):
""" """
eid = -1 eid = -1
try: try:
eid = kwargs.pop("_id", -1) eid = kwargs.pop("id", -1)
except: pass except:
e = entity_instance((self.schema, type)) pass
e = entity_instance((self.schema, type), self)
self.wrapped_data.add(e.wrapped_data, eid) self.wrapped_data.add(e.wrapped_data, eid)
e.wrapped_data.this.disown() e.wrapped_data.this.disown()
attrs = list(enumerate(args)) + [(e.wrapped_data.get_argument_index(name), arg) for name, arg in kwargs.items()] attrs = list(enumerate(args)) + [(e.wrapped_data.get_argument_index(name), arg) for name, arg in kwargs.items()]
for idx, arg in attrs: for idx, arg in attrs:
e[idx] = arg e[idx] = arg
if self.transaction:
self.transaction.store_create(e)
return e return e
def __getattr__(self, attr): def __getattr__(self, attr):
@@ -100,9 +241,9 @@ class file(object):
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, numbers.Integral): if isinstance(key, numbers.Integral):
return entity_instance(self.wrapped_data.by_id(key)) return entity_instance(self.wrapped_data.by_id(key), self)
elif isinstance(key, basestring): elif isinstance(key, basestring):
return entity_instance(self.wrapped_data.by_guid(str(key))) return entity_instance(self.wrapped_data.by_guid(str(key)), self)
def by_id(self, id): def by_id(self, id):
"""Return an IFC entity instance filtered by IFC ID. """Return an IFC entity instance filtered by IFC ID.
@@ -129,7 +270,7 @@ class file(object):
If the entity already exists, it is not re-added.""" If the entity already exists, it is not re-added."""
inst.wrapped_data.this.disown() inst.wrapped_data.this.disown()
return entity_instance(self.wrapped_data.add(inst.wrapped_data, -1 if _id is None else _id)) return entity_instance(self.wrapped_data.add(inst.wrapped_data, -1 if _id is None else _id), self)
def by_type(self, type, include_subtypes=True): def by_type(self, type, include_subtypes=True):
"""Return IFC objects filtered by IFC Type and wrapped with the entity_instance class. """Return IFC objects filtered by IFC Type and wrapped with the entity_instance class.
@@ -144,8 +285,8 @@ class file(object):
:rtype: list :rtype: list
""" """
if include_subtypes: if include_subtypes:
return [entity_instance(e) for e in self.wrapped_data.by_type(type)] return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)]
return [entity_instance(e) for e in self.wrapped_data.by_type_excl_subtypes(type)] return [entity_instance(e, self) for e in self.wrapped_data.by_type_excl_subtypes(type)]
def traverse(self, inst, max_levels=None): def traverse(self, inst, max_levels=None):
"""Get a list of all referenced instances for a particular instance including itself """Get a list of all referenced instances for a particular instance including itself
@@ -159,7 +300,7 @@ class file(object):
""" """
if max_levels is None: if max_levels is None:
max_levels = -1 max_levels = -1
return [entity_instance(e) for e in self.wrapped_data.traverse(inst.wrapped_data, max_levels)] return [entity_instance(e, self) for e in self.wrapped_data.traverse(inst.wrapped_data, max_levels)]
def get_inverse(self, inst): def get_inverse(self, inst):
"""Return a list of entities that reference this entity """Return a list of entities that reference this entity
@@ -169,7 +310,7 @@ class file(object):
:returns: A list of ifcopenshell.entity_instance.entity_instance objects :returns: A list of ifcopenshell.entity_instance.entity_instance objects
:rtype: list :rtype: list
""" """
return [entity_instance(e) for e in self.wrapped_data.get_inverse(inst.wrapped_data)] return [entity_instance(e, self) for e in self.wrapped_data.get_inverse(inst.wrapped_data)]
def remove(self, inst): def remove(self, inst):
"""Deletes an IFC object in the file. """Deletes an IFC object in the file.
@@ -182,12 +323,14 @@ class file(object):
:type inst: ifcopenshell.entity_instance.entity_instance :type inst: ifcopenshell.entity_instance.entity_instance
:rtype: None :rtype: None
""" """
if self.transaction:
self.transaction.store_delete(inst)
return self.wrapped_data.remove(inst.wrapped_data) return self.wrapped_data.remove(inst.wrapped_data)
def batch(self): def batch(self):
"""Low-level mechanism to speed up deletion of large subgraphs""" """Low-level mechanism to speed up deletion of large subgraphs"""
return self.wrapped_data.batch() return self.wrapped_data.batch()
def unbatch(self): def unbatch(self):
"""Low-level mechanism to speed up deletion of large subgraphs""" """Low-level mechanism to speed up deletion of large subgraphs"""
return self.wrapped_data.unbatch() return self.wrapped_data.unbatch()