mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user