Integrate brickschema undo system into BlenderBIM Add-on undo system wrapper

This commit is contained in:
Dion Moult
2023-07-11 11:16:35 +10:00
parent 88defa07ce
commit 562c775da2
6 changed files with 109 additions and 69 deletions
+14 -5
View File
@@ -26,6 +26,7 @@ import ifcopenshell
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
from blenderbim.tool.brick import BrickStore
class IfcStore:
@@ -405,7 +406,10 @@ class IfcStore:
if is_top_level_operator:
IfcStore.begin_transaction(operator)
IfcStore.get_file().begin_transaction()
if tool.Ifc.get():
tool.Ifc.get().begin_transaction()
if BrickStore.graph:
BrickStore.begin_transaction()
# This empty transaction ensures that each operator has at least one transaction
IfcStore.add_transaction_operation(operator, rollback=lambda data: True, commit=lambda data: True)
else:
@@ -417,10 +421,13 @@ class IfcStore:
result = getattr(operator, "_execute")(context)
if is_top_level_operator:
IfcStore.get_file().end_transaction()
IfcStore.add_transaction_operation(
operator, rollback=lambda d: IfcStore.get_file().undo(), commit=lambda d: IfcStore.get_file().redo()
)
if tool.Ifc.get():
tool.Ifc.get().end_transaction()
IfcStore.add_transaction_operation(
operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
)
if BrickStore.graph:
BrickStore.end_transaction()
IfcStore.end_transaction(operator)
blenderbim.bim.handler.refresh_ui_data()
@@ -456,6 +463,7 @@ class IfcStore:
@staticmethod
def undo():
BrickStore.undo()
if not IfcStore.history:
return
event = IfcStore.history.pop()
@@ -465,6 +473,7 @@ class IfcStore:
@staticmethod
def redo():
BrickStore.redo()
if not IfcStore.future:
return
event = IfcStore.future.pop()
@@ -33,8 +33,6 @@ classes = (
operator.RewindBrickClass,
operator.ViewBrickClass,
operator.ViewBrickItem,
operator.UndoBrick,
operator.RedoBrick,
operator.SerializeBrick,
operator.AddBrickNamespace,
prop.Brick,
@@ -24,9 +24,10 @@ import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
from blenderbim.tool.brick import BrickStore
class Operator:
def execute(self, context):
self._execute(context)
IfcStore.execute_ifc_operator(self, context)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -126,7 +127,7 @@ class AddBrick(bpy.types.Operator, Operator):
namespace=props.namespace,
brick_class=props.brick_equipment_class,
library=library,
label=props.new_brick_label
label=props.new_brick_label,
)
@@ -159,14 +160,36 @@ class ConvertIfcToBrick(bpy.types.Operator, Operator):
core.convert_ifc_to_brick(tool.Brick, namespace=props.namespace, library=library)
class NewBrickFile(bpy.types.Operator, Operator):
class NewBrickFile(bpy.types.Operator):
bl_idname = "bim.new_brick_file"
bl_label = "New Brick File"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
IfcStore.begin_transaction(self)
IfcStore.add_transaction_operation(self, rollback=self.rollback, commit=lambda data: True)
self._execute(context)
self.transaction_data = {
"schema": BrickStore.schema,
"path": BrickStore.path,
"graph": BrickStore.graph,
}
IfcStore.add_transaction_operation(self, rollback=lambda data: True, commit=self.commit)
IfcStore.end_transaction(self)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
def _execute(self, context):
core.new_brick_file(tool.Brick)
def rollback(self, data):
BrickStore.purge()
def commit(self, data):
BrickStore.schema = data["schema"]
BrickStore.path = data["path"]
BrickStore.graph = data["graph"]
class RefreshBrickViewer(bpy.types.Operator, Operator):
bl_idname = "bim.refresh_brick_viewer"
@@ -191,21 +214,8 @@ class RemoveBrick(bpy.types.Operator, Operator):
brick_uri=props.bricks[props.active_brick_index].uri,
)
class UndoBrick(bpy.types.Operator, Operator):
bl_idname = "bim.undo_brick"
bl_label = "Undo Brick"
def _execute(self, context):
core.undo_brick(tool.Brick)
class RedoBrick(bpy.types.Operator, Operator):
bl_idname = "bim.redo_brick"
bl_label = "Redo Brick"
def _execute(self, context):
core.redo_brick(tool.Brick)
class SerializeBrick(bpy.types.Operator, Operator):
class SerializeBrick(bpy.types.Operator):
bl_idname = "bim.serialize_brick"
bl_label = "Serialize Brick"
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
@@ -220,18 +230,19 @@ class SerializeBrick(bpy.types.Operator, Operator):
else:
return self.execute(context)
def _execute(self, context):
def execute(self, context):
if self.should_save_as or not BrickStore.path:
BrickStore.path = self.filepath
core.serialize_brick(tool.Brick)
return {"FINISHED"}
@classmethod
def description(cls, context, properties):
if properties.should_save_as:
return "Save Brick project to a selected file"
return "Save the Brick project"
class AddBrickNamespace(bpy.types.Operator, Operator):
bl_idname = "bim.add_brick_namespace"
bl_label = "Add Brick Namespace"
@@ -240,4 +251,4 @@ class AddBrickNamespace(bpy.types.Operator, Operator):
props = context.scene.BIMBrickProperties
alias = props.new_brick_namespace_alias
uri = props.new_brick_namespace_uri
core.add_namespace(tool.Brick, alias=alias, uri=uri)
core.add_namespace(tool.Brick, alias=alias, uri=uri)
@@ -87,10 +87,6 @@ class BIM_PT_brickschema(Panel):
row.operator("bim.add_brick_feed", text="", icon="PLUGIN")
row.operator("bim.remove_brick", text="", icon="X")
row = self.layout.row(align=True)
row.operator("bim.undo_brick", icon="LOOP_BACK")
row.operator("bim.redo_brick", icon="LOOP_FORWARDS")
row = self.layout.row(align=True)
op = row.operator("bim.serialize_brick", icon="EXPORT", text="Save")
op.should_save_as = False
+1 -10
View File
@@ -112,18 +112,9 @@ def remove_brick(ifc, brick, library=None, brick_uri=None):
brick.run_refresh_brick_viewer()
def undo_brick(brick):
brick.undo_brick()
brick.run_refresh_brick_viewer()
def redo_brick(brick):
brick.redo_brick()
brick.run_refresh_brick_viewer()
def serialize_brick(brick):
brick.serialize_brick()
def add_namespace(brick, alias=None, uri=None):
brick.add_namespace(alias, uri)
+62 -27
View File
@@ -22,6 +22,7 @@ import ifcopenshell
import ifcopenshell.util.brick
import blenderbim.core.tool
import blenderbim.tool as tool
from contextlib import contextmanager
try:
import brickschema
@@ -36,15 +37,17 @@ except:
# silence known rdflib_sqlalchemy TypeError warning
# see https://github.com/BrickSchema/Brick/issues/513#issuecomment-1558493675
import logging
logger = logging.getLogger("rdflib")
logger.setLevel(logging.ERROR)
class Brick(blenderbim.core.tool.Brick):
@classmethod
def add_brick(cls, namespace, brick_class, label):
ns = Namespace(namespace)
brick = ns[ifcopenshell.guid.expand(ifcopenshell.guid.new())]
with BrickStore.graph.new_changeset("PROJECT") as cs:
with BrickStore.new_changeset() as cs:
cs.add((brick, RDF.type, URIRef(brick_class)))
cs.add((brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal(label)))
return str(brick)
@@ -256,7 +259,7 @@ class Brick(blenderbim.core.tool.Brick):
@classmethod
def load_brick_file(cls, filepath):
if not BrickStore.schema: # important check for running under test cases
if not BrickStore.schema: # important check for running under test cases
cwd = os.path.dirname(os.path.realpath(__file__))
BrickStore.schema = os.path.join(cwd, "..", "bim", "schema", "Brick.ttl")
BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://")
@@ -268,7 +271,7 @@ class Brick(blenderbim.core.tool.Brick):
@classmethod
def new_brick_file(cls):
if not BrickStore.schema: # important check for running under test cases
if not BrickStore.schema: # important check for running under test cases
cwd = os.path.dirname(os.path.realpath(__file__))
BrickStore.schema = os.path.join(cwd, "..", "bim", "schema", "Brick.ttl")
BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://")
@@ -286,8 +289,8 @@ class Brick(blenderbim.core.tool.Brick):
@classmethod
def remove_brick(cls, brick_uri):
if(BrickStore.graph.triples((URIRef(brick_uri), None, None))):
with BrickStore.graph.new_changeset("PROJECT") as cs:
if BrickStore.graph.triples((URIRef(brick_uri), None, None)):
with BrickStore.new_changeset() as cs:
for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)):
cs.remove(triple)
@@ -315,41 +318,73 @@ class Brick(blenderbim.core.tool.Brick):
def set_active_brick_class(cls, brick_class):
bpy.context.scene.BIMBrickProperties.active_brick_class = brick_class
@classmethod
def undo_brick(cls):
if(len(BrickStore.graph.versions()) > 1):
BrickStore.graph.undo()
@classmethod
def redo_brick(cls):
with BrickStore.graph.conn() as conn:
redo_record = conn.execute(
"SELECT * from redos " "ORDER BY timestamp ASC LIMIT 1"
).fetchone()
if redo_record is not None:
BrickStore.graph.redo()
@classmethod
def serialize_brick(cls):
BrickStore.get_project().serialize(destination=BrickStore.path, format="turtle")
@classmethod
def add_namespace(cls, alias, uri):
BrickStore.graph.bind(alias, Namespace(uri))
# need some way to reload namespace enum view
class BrickStore:
schema = None # this is now a os path
path = None # file path if the project was loaded in
schema = None # this is now a os path
path = None # file path if the project was loaded in
graph = None # this is the VersionedGraphCollection with 2 arbitrarily named graphs: "schema" and "project"
# "SCHEMA" holds the Brick.ttl metadata; "PROJECT" holds all the authored entities
# "SCHEMA" holds the Brick.ttl metadata; "PROJECT" holds all the authored entities
history = []
future = []
current_changesets = 0
history_size = 64
@staticmethod
def purge():
BrickStore.schema = None
BrickStore.graph = None
BrickStore.path = None
BrickStore.path = None
@classmethod
def get_project(cls):
return BrickStore.graph.graph_at(graph="PROJECT")
return BrickStore.graph.graph_at(graph="PROJECT")
@classmethod
def set_history_size(cls, size):
cls.history_size = size
while len(cls.history) > cls.history_size:
cls.history.pop(0)
@classmethod
def begin_transaction(cls):
cls.current_changesets = 0
@classmethod
def end_transaction(cls):
cls.history.append(cls.current_changesets)
if len(cls.history) > cls.history_size:
cls.history.pop(0)
@classmethod
@contextmanager
def new_changeset(cls):
cls.current_changesets += 1
with BrickStore.graph.new_changeset("PROJECT") as cs:
yield cs
@classmethod
def undo(cls):
if not BrickStore.graph or not BrickStore.history:
return
total_changesets = BrickStore.history.pop()
for i in range(0, total_changesets):
BrickStore.graph.undo()
BrickStore.future.append(total_changesets)
@classmethod
def redo(cls):
if not BrickStore.graph or not BrickStore.future:
return
total_changesets = BrickStore.future.pop()
for i in range(0, total_changesets):
BrickStore.graph.redo()
BrickStore.history.append(total_changesets)