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:
bpy.utils.register_class(cls)
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.loadIfcStore)
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.name = obj.name
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]
grid_collection = None
for collection in bpy.data.collections:
@@ -103,14 +103,26 @@ def loadIfcStore(scene):
if not ifc_file:
return
IfcStore.get_schema()
[
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
]
IfcStore.reload_linked_elements()
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
def ensureIfcExported(scene):
if IfcStore.get_file() and not bpy.context.scene.BIMProperties.ifc_file:
+59
View File
@@ -1,4 +1,5 @@
import bpy
import uuid
import ifcopenshell
import blenderbim.bim.handler
@@ -15,6 +16,9 @@ class IfcStore:
library_path = ""
library_file = None
element_listeners = set()
last_transaction = ""
history = []
future = []
@staticmethod
def purge():
@@ -65,6 +69,21 @@ class IfcStore:
def add_element_listener(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
def link_element(element, obj):
IfcStore.id_map[element.id()] = obj
@@ -100,3 +119,43 @@ class IfcStore:
if obj:
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)
# 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
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.
pass
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):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
@@ -45,6 +46,7 @@ class EnableEditingAttributes(bpy.types.Operator):
class DisableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
@@ -61,6 +63,8 @@ class DisableEditingAttributes(bpy.types.Operator):
class EditAttributes(bpy.types.Operator):
bl_idname = "bim.edit_attributes"
bl_label = "Edit Attributes"
bl_options = {"REGISTER", "UNDO"}
transaction_key: bpy.props.StringProperty()
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
@@ -95,9 +99,11 @@ class EditAttributes(bpy.types.Operator):
elif attribute["type"] == "enum":
attributes[attribute["name"]] = blender_attribute.enum_value
product = self.file.by_id(oprops.ifc_definition_id)
self.file.begin_transaction()
ifcopenshell.api.run(
"attribute.edit_attributes", self.file, **{"product": product, "attributes": attributes}
)
self.file.end_transaction()
if "Name" in attributes:
new_name = "{}/{}".format(product.is_a(), product.Name or "Unnamed")
collection = bpy.data.collections.get(obj.name)
@@ -106,12 +112,23 @@ class EditAttributes(bpy.types.Operator):
obj.name = new_name
Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
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"}
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):
bl_idname = "bim.generate_global_id"
bl_label = "Regenerate GlobalId"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
index = bpy.context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
@@ -2,6 +2,7 @@ import bpy
from . import ui, prop, operator
classes = (
operator.ValidateIfcFile,
operator.ProfileImportIFC,
operator.CreateAllShapes,
operator.CreateShapeFromStepId,
@@ -5,6 +5,19 @@ import blenderbim.bim.import_ifc as import_ifc
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):
bl_idname = "bim.profile_import_ifc"
bl_label = "Profile Import IFC"
@@ -4,7 +4,6 @@ from . import ui, prop, operator
classes = (
operator.CreateProject,
operator.CreateProjectLibrary,
operator.ValidateIfcFile,
operator.SelectLibraryFile,
operator.ChangeLibraryElement,
operator.RefreshLibrary,
@@ -2,6 +2,7 @@ import bpy
import logging
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.representation
import bpy
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
@@ -11,6 +12,8 @@ from blenderbim.bim import import_ifc
class CreateProject(bpy.types.Operator):
bl_idname = "bim.create_project"
bl_label = "Create Project"
bl_options = {"REGISTER", "UNDO"}
transaction_key: bpy.props.StringProperty()
def execute(self, context):
self.file = IfcStore.get_file()
@@ -22,6 +25,9 @@ class CreateProject(bpy.types.Operator):
)
self.file = IfcStore.get_file()
self.transaction_data = {"file": self.file}
IfcStore.add_transaction(self)
bpy.ops.bim.add_person()
bpy.ops.bim.add_organisation()
@@ -30,7 +36,7 @@ class CreateProject(bpy.types.Operator):
building = bpy.data.objects.new("My Building", 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.add_subcontext(context="Model")
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", subcontext="Annotation", target_view="PLAN_VIEW")
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
if subcontext.ContextIdentifier == "Body":
bpy.context.scene.BIMProperties.contexts = str(subcontext.id())
break
bpy.context.scene.BIMProperties.contexts = str(
ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW").id()
)
bpy.ops.bim.assign_class(obj=site.name, ifc_class="IfcSite")
bpy.ops.bim.assign_class(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=site.name, ifc_class="IfcSite")
bpy.ops.bim.assign_class(transaction_key=self.transaction_key, obj=building.name, ifc_class="IfcBuilding")
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=building.name, relating_object=site.name)
bpy.ops.bim.assign_object(related_object=building_storey.name, relating_object=building.name)
# Data.load()
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):
bl_idname = "bim.create_project_library"
bl_label = "Create Project Library"
bl_options = {"REGISTER", "UNDO"}
transaction_key: bpy.props.StringProperty()
def execute(self, context):
self.file = IfcStore.get_file()
@@ -67,45 +84,62 @@ class CreateProjectLibrary(bpy.types.Operator):
)
self.file = IfcStore.get_file()
self.transaction_data = {"file": self.file}
IfcStore.add_transaction(self)
if self.file.schema == "IFC2X3":
bpy.ops.bim.add_person()
bpy.ops.bim.add_organisation()
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()
return {"FINISHED"}
def rollback(self, data):
IfcStore.file = None
blenderbim.bim.handler.purge_module_data()
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"}
def commit(self, data):
blenderbim.bim.handler.purge_module_data()
IfcStore.file = data["file"]
class SelectLibraryFile(bpy.types.Operator):
bl_idname = "bim.select_library_file"
bl_label = "Select Library File"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
def execute(self, context):
old_filepath = IfcStore.library_path
IfcStore.library_path = self.filepath
IfcStore.library_file = ifcopenshell.open(self.filepath)
bpy.ops.bim.refresh_library()
self.transaction_data = {"old_filepath": old_filepath, "filepath": self.filepath}
IfcStore.add_transaction(self)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
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):
bl_idname = "bim.refresh_library"
@@ -132,6 +166,7 @@ class RefreshLibrary(bpy.types.Operator):
class ChangeLibraryElement(bpy.types.Operator):
bl_idname = "bim.change_library_element"
bl_label = "Change Library Element"
bl_options = {"REGISTER", "UNDO"}
element_name: bpy.props.StringProperty()
def execute(self, context):
@@ -163,6 +198,7 @@ class ChangeLibraryElement(bpy.types.Operator):
class RewindLibrary(bpy.types.Operator):
bl_idname = "bim.rewind_library"
bl_label = "Rewind Library"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.props = context.scene.BIMProjectProperties
@@ -180,40 +216,62 @@ class RewindLibrary(bpy.types.Operator):
class AssignLibraryDeclaration(bpy.types.Operator):
bl_idname = "bim.assign_library_declaration"
bl_label = "Assign Library Declaration"
bl_options = {"REGISTER", "UNDO"}
definition: bpy.props.IntProperty()
def execute(self, context):
self.props = context.scene.BIMProjectProperties
self.file = IfcStore.library_file
self.file.begin_transaction()
ifcopenshell.api.run(
"project.assign_declaration",
IfcStore.library_file,
definition=IfcStore.library_file.by_id(self.definition),
relating_context=IfcStore.library_file.by_type("IfcProjectLibrary")[0],
self.file,
definition=self.file.by_id(self.definition),
relating_context=self.file.by_type("IfcProjectLibrary")[0],
)
self.file.end_transaction()
element_name = self.props.active_library_element
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"}
def rollback(self, data):
IfcStore.library_file.undo()
def commit(self, data):
IfcStore.library_file.redo()
class UnassignLibraryDeclaration(bpy.types.Operator):
bl_idname = "bim.unassign_library_declaration"
bl_label = "Unassign Library Declaration"
bl_options = {"REGISTER", "UNDO"}
definition: bpy.props.IntProperty()
def execute(self, context):
self.props = context.scene.BIMProjectProperties
self.file = IfcStore.library_file
self.file.begin_transaction()
ifcopenshell.api.run(
"project.unassign_declaration",
IfcStore.library_file,
definition=IfcStore.library_file.by_id(self.definition),
relating_context=IfcStore.library_file.by_type("IfcProjectLibrary")[0],
self.file,
definition=self.file.by_id(self.definition),
relating_context=self.file.by_type("IfcProjectLibrary")[0],
)
self.file.end_transaction()
element_name = self.props.active_library_element
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"}
def rollback(self, data):
IfcStore.library_file.undo()
def commit(self, data):
IfcStore.library_file.redo()
class SaveLibraryFile(bpy.types.Operator):
bl_idname = "bim.save_library_file"
@@ -227,17 +285,22 @@ class SaveLibraryFile(bpy.types.Operator):
class AppendLibraryElement(bpy.types.Operator):
bl_idname = "bim.append_library_element"
bl_label = "Append Library Element"
bl_options = {"REGISTER", "UNDO"}
definition: bpy.props.IntProperty()
def execute(self, context):
self.file = IfcStore.get_file()
self.file.begin_transaction()
element = ifcopenshell.api.run(
"project.append_asset",
IfcStore.get_file(),
self.file,
library=IfcStore.library_file,
element=IfcStore.library_file.by_id(self.definition),
)
self.file.end_transaction()
self.import_type_from_ifc(element)
blenderbim.bim.handler.purge_module_data()
IfcStore.add_transaction(self)
return {"FINISHED"}
def import_type_from_ifc(self, element):
@@ -259,10 +322,17 @@ class AppendLibraryElement(bpy.types.Operator):
ifc_importer.create_type_product(element)
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):
bl_idname = "bim.enable_editing_header"
bl_label = "Enable Editing Header"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.file = IfcStore.get_file()
@@ -294,23 +364,53 @@ class EnableEditingHeader(bpy.types.Operator):
class EditHeader(bpy.types.Operator):
bl_idname = "bim.edit_header"
bl_label = "Edit Header"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMProjectProperties
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.organization = (props.organisation_name, props.organisation_email)
self.file.wrapped_data.header.file_name.authorization = props.authorisation
bpy.ops.bim.disable_editing_header()
self.transaction_data["new"] = self.record_state()
IfcStore.add_transaction(self)
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):
bl_idname = "bim.disable_editing_header"
bl_label = "Disable Editing Header"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.scene.BIMProjectProperties.is_editing = False
@@ -78,6 +78,7 @@ class AssignClass(bpy.types.Operator):
bl_idname = "bim.assign_class"
bl_label = "Assign IFC Class"
bl_options = {"REGISTER", "UNDO"}
transaction_key: bpy.props.StringProperty()
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty()
predefined_type: bpy.props.StringProperty()
@@ -87,6 +88,7 @@ class AssignClass(bpy.types.Operator):
ifc_representation_class: bpy.props.StringProperty()
def execute(self, context):
self.transaction_data = []
objects = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects
self.file = IfcStore.get_file()
self.declaration = IfcStore.get_schema().declaration_by_name(self.ifc_class)
@@ -96,11 +98,13 @@ class AssignClass(bpy.types.Operator):
predefined_type = None
for obj in objects:
self.assign_class(context, obj)
IfcStore.add_transaction(self)
return {"FINISHED"}
def assign_class(self, context, obj):
if obj.BIMObjectProperties.ifc_definition_id:
return
self.file.begin_transaction()
product = ifcopenshell.api.run(
"root.create_entity",
self.file,
@@ -110,8 +114,10 @@ class AssignClass(bpy.types.Operator):
"name": obj.name,
},
)
self.file.end_transaction()
obj.name = "{}/{}".format(product.is_a(), obj.name)
IfcStore.link_element(product, obj)
self.transaction_data.append({"element": product.id(), "obj": obj.name})
if self.should_add_representation:
bpy.ops.bim.add_representation(
@@ -171,6 +177,20 @@ class AssignClass(bpy.types.Operator):
)
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):
bl_idname = "bim.unassign_class"
@@ -301,7 +301,7 @@ class BIMWorkScheduleProperties(PropertyGroup):
class BIMTaskTreeProperties(PropertyGroup):
# 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)
+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
)
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")
last_transaction: StringProperty(name="Last Transaction")
contexts: EnumProperty(items=getContexts, name="Contexts")
available_contexts: EnumProperty(items=[("Model", "Model", ""), ("Plan", "Plan", "")], name="Available Contexts")
available_subcontexts: EnumProperty(items=getSubcontexts, name="Available Subcontexts")
+178 -50
View File
@@ -1,91 +1,219 @@
Installation
============
BlenderBIM is packaged as a 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.
There are different methods of installation, depending on your situation.
1. **Packaged installation** is recommended for regular users.
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
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
folder depends on which operating system you use. On Linux:
::
~/.config/blender/2.81/scripts/addons/
~/.config/blender/2.XX/scripts/addons/
On Mac:
::
/Users/{YOUR_USER}/Library/Application Support/Blender/2.81/
/Users/{YOUR_USER}/Library/Application Support/Blender/2.XX/
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
BlenderBIM has a variety of complex dependencies. A full list is below:
Upon installation, the BlenderBIM Add-on is stored in the ``blenderbim/``
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/
ifcopenshell/
OCC/
pystache/
svgwrite/
deepdiff/
jsonpickle/
lib/ # Note: this only exists on MacOS and Linux
ordered_set.py
pyparsing.py
bim/ (core code)
libs/ (dependencies)
__init__.py
If you are not on Windows, when BlenderBIM first launches, it will create a
bunch of library files in the ``2.81/`` folder too. This is a non-standard
location to place files, but is a hack to allow people to use precompiled builds
from Conda.
This corresponds to the structure found in the source code `here
<https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.6.0/src/blenderbim/blenderbim>`__.
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
package for the wrong platform.
Updating
--------
It is recommended to uninstall the current BlenderBIM add-on before installing
the latest version to ensure the update goes well.
From Source
-----------
Source installation
-------------------
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
doesn't require any compilation. First, install the latest official release, and
then `download the latest source code
<https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.6.0.zip>`_. If you know
how to use Git, you can also stay up to date like so:
doesn't require any compilation.
You can create your own package by using the Makefile as shown below. You can
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
$ cd IfcOpenShell
$ git checkout v0.6.0
Then, just copy the files from the source code's
``src/blenderbim/blenderbim/`` folder and replace the files in your
Blender add-on's ``blenderbim/`` folder.
# Remove the Blender add-on Python code
$ rm -r /path/to/blender/2.XX/scripts/addons/blenderbim/bim/
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.
# Replace them with links to the Git repository
$ ln -s src/blenderbim/blenderbim/bim /path/to/blender/2.XX/scripts/addons/blenderbim/bim
# 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
------------
You can remove all of the files added by BlenderBIM in the Blender add-ons
folder and then remove the add-on using the Blender interface through ``Edit >
Preferences > Add-ons`` just like any other add-on.
Navigate to ``Edit > Preferences > Add-ons``. Due to a limitation in Blender,
you have to first disable the BlenderBIM Add-on in your Blender preferences by
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
``2.81/`` directory. Do not delete any of Blender's own folders.
Alternatively, you may uninstall manually by deleting the ``blenderbim/``
directory in your Blender add-ons directory.
@@ -48,20 +48,21 @@ class entity_instance(object):
>>> #423=IfcProductDefinitionShape($,$,(#409,#421))
"""
def __init__(self, e):
def __init__(self, e, file):
if isinstance(e, tuple):
e = ifcopenshell_wrapper.new_IfcBaseClass(*e)
super(entity_instance, self).__setattr__("wrapped_data", e)
self.wrapped_data.file = file
def __getattr__(self, name):
INVALID, FORWARD, INVERSE = range(3)
attr_cat = self.wrapped_data.get_attribute_category(name)
if attr_cat == FORWARD:
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:
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:
raise AttributeError(
"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
@staticmethod
def wrap_value(v):
def wrap_value(v, file):
def wrap(e):
return entity_instance(e)
return entity_instance(e, file)
def is_instance(e):
return isinstance(e, ifcopenshell_wrapper.entity_instance)
@@ -116,12 +117,15 @@ class entity_instance(object):
return self.wrapped_data.get_argument_name(attr_idx)
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):
if key < 0 or key >= len(self):
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):
attr_type = real_attr_type = self.attribute_type(idx).title().replace(" ", "")
@@ -271,7 +275,7 @@ class entity_instance(object):
return return_type(_())
__dict__ = property(get_info)
def get_info_2(self, include_identifier=True, recursive=False, return_type=dict, ignore=()):
assert include_identifier
assert recursive
+155 -12
View File
@@ -35,6 +35,105 @@ except NameError:
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):
"""Base class for containing IFC files.
@@ -59,6 +158,45 @@ class file(object):
args = filter(None, [schema])
args = map(ifcopenshell_wrapper.schema_by_name, 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):
"""Create a new IFC entity in the file.
@@ -82,14 +220,17 @@ class file(object):
"""
eid = -1
try:
eid = kwargs.pop("_id", -1)
except: pass
e = entity_instance((self.schema, type))
eid = kwargs.pop("id", -1)
except:
pass
e = entity_instance((self.schema, type), self)
self.wrapped_data.add(e.wrapped_data, eid)
e.wrapped_data.this.disown()
attrs = list(enumerate(args)) + [(e.wrapped_data.get_argument_index(name), arg) for name, arg in kwargs.items()]
for idx, arg in attrs:
e[idx] = arg
if self.transaction:
self.transaction.store_create(e)
return e
def __getattr__(self, attr):
@@ -100,9 +241,9 @@ class file(object):
def __getitem__(self, key):
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):
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):
"""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."""
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):
"""Return IFC objects filtered by IFC Type and wrapped with the entity_instance class.
@@ -144,8 +285,8 @@ class file(object):
:rtype: list
"""
if include_subtypes:
return [entity_instance(e) 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(type)]
return [entity_instance(e, self) for e in self.wrapped_data.by_type_excl_subtypes(type)]
def traverse(self, inst, max_levels=None):
"""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:
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):
"""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
: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):
"""Deletes an IFC object in the file.
@@ -182,12 +323,14 @@ class file(object):
:type inst: ifcopenshell.entity_instance.entity_instance
:rtype: None
"""
if self.transaction:
self.transaction.store_delete(inst)
return self.wrapped_data.remove(inst.wrapped_data)
def batch(self):
"""Low-level mechanism to speed up deletion of large subgraphs"""
return self.wrapped_data.batch()
def unbatch(self):
"""Low-level mechanism to speed up deletion of large subgraphs"""
return self.wrapped_data.unbatch()