diff --git a/multi_user/__init__.py b/multi_user/__init__.py
index 0d5577b..1533252 100644
--- a/multi_user/__init__.py
+++ b/multi_user/__init__.py
@@ -59,6 +59,7 @@ def register():
from . import presence
from . import operators
+ from . import handlers
from . import ui
from . import preferences
from . import addon_updater_ops
@@ -67,6 +68,7 @@ def register():
addon_updater_ops.register(bl_info)
presence.register()
operators.register()
+ handlers.register()
ui.register()
except ModuleNotFoundError as e:
raise Exception(module_error_msg)
@@ -87,6 +89,7 @@ def register():
def unregister():
from . import presence
from . import operators
+ from . import handlers
from . import ui
from . import preferences
from . import addon_updater_ops
@@ -96,6 +99,7 @@ def unregister():
presence.unregister()
addon_updater_ops.unregister()
ui.unregister()
+ handlers.unregister()
operators.unregister()
preferences.unregister()
diff --git a/multi_user/handlers.py b/multi_user/handlers.py
new file mode 100644
index 0000000..f5033c9
--- /dev/null
+++ b/multi_user/handlers.py
@@ -0,0 +1,150 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# ##### END GPL LICENSE BLOCK #####
+
+import logging
+
+import bpy
+from bpy.app.handlers import persistent
+from replication import porcelain
+from replication.constants import RP_COMMON, STATE_ACTIVE, STATE_SYNCING, UP
+from replication.exception import ContextError, NonAuthorizedOperationError
+from replication.interface import session
+
+from . import shared_data, utils
+
+
+def sanitize_deps_graph(remove_nodes: bool = False):
+ """ Cleanup the replication graph
+ """
+ if session and session.state == STATE_ACTIVE:
+ start = utils.current_milli_time()
+ rm_cpt = 0
+ for node in session.repository.graph.values():
+ node.instance = session.repository.rdp.resolve(node.data)
+ if node is None \
+ or (node.state == UP and not node.instance):
+ if remove_nodes:
+ try:
+ porcelain.rm(session.repository,
+ node.uuid,
+ remove_dependencies=False)
+ logging.info(f"Removing {node.uuid}")
+ rm_cpt += 1
+ except NonAuthorizedOperationError:
+ continue
+ logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
+
+
+def update_external_dependencies():
+ """Force external dependencies(files such as images) evaluation
+ """
+ nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']]
+ for node_id in nodes_ids:
+ node = session.repository.graph.get(node_id)
+ if node and node.owner in [session.repository.username, RP_COMMON]:
+ porcelain.commit(session.repository, node_id)
+ porcelain.push(session.repository, 'origin', node_id)
+
+
+@persistent
+def on_scene_update(scene):
+ """Forward blender depsgraph update to replication
+ """
+ if session and session.state == STATE_ACTIVE:
+ context = bpy.context
+ blender_depsgraph = bpy.context.view_layer.depsgraph
+ dependency_updates = [u for u in blender_depsgraph.updates]
+ settings = utils.get_preferences()
+ incoming_updates = shared_data.session.applied_updates
+
+ distant_update = [getattr(u.id, 'uuid', None) for u in dependency_updates if getattr(u.id, 'uuid', None) in incoming_updates]
+ if distant_update:
+ for u in distant_update:
+ shared_data.session.applied_updates.remove(u)
+ logging.info(f"Ignoring distant update of {dependency_updates[0].id.name}")
+ return
+
+ update_external_dependencies()
+
+ # NOTE: maybe we don't need to check each update but only the first
+ for update in reversed(dependency_updates):
+ update_uuid = getattr(update.id, 'uuid', None)
+ if update_uuid:
+ node = session.repository.graph.get(update.id.uuid)
+ check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
+
+ if node and (node.owner == session.repository.username or check_common):
+ logging.debug(f"Evaluate {update.id.name}")
+ if node.state == UP:
+ try:
+ porcelain.commit(session.repository, node.uuid)
+ porcelain.push(session.repository,
+ 'origin', node.uuid)
+ except ReferenceError:
+ logging.debug(f"Reference error {node.uuid}")
+ except ContextError as e:
+ logging.debug(e)
+ except Exception as e:
+ logging.error(e)
+ else:
+ continue
+ elif isinstance(update.id, bpy.types.Scene):
+ scn_uuid = porcelain.add(session.repository, update.id)
+ porcelain.commit(session.repository, scn_uuid)
+ porcelain.push(session.repository, 'origin', scn_uuid)
+
+
+@persistent
+def resolve_deps_graph(dummy):
+ """Resolve deps graph
+
+ Temporary solution to resolve each node pointers after a Undo.
+ A future solution should be to avoid storing dataclock reference...
+
+ """
+ if session and session.state == STATE_ACTIVE:
+ sanitize_deps_graph(remove_nodes=True)
+
+
+@persistent
+def load_pre_handler(dummy):
+ if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
+ bpy.ops.session.stop()
+
+
+@persistent
+def update_client_frame(scene):
+ if session and session.state == STATE_ACTIVE:
+ porcelain.update_user_metadata(session.repository, {
+ 'frame_current': scene.frame_current
+ })
+
+
+def register():
+ bpy.app.handlers.undo_post.append(resolve_deps_graph)
+ bpy.app.handlers.redo_post.append(resolve_deps_graph)
+
+ bpy.app.handlers.load_pre.append(load_pre_handler)
+ bpy.app.handlers.frame_change_pre.append(update_client_frame)
+
+
+def unregister():
+ bpy.app.handlers.undo_post.remove(resolve_deps_graph)
+ bpy.app.handlers.redo_post.remove(resolve_deps_graph)
+
+ bpy.app.handlers.load_pre.remove(load_pre_handler)
+ bpy.app.handlers.frame_change_pre.remove(update_client_frame)
diff --git a/multi_user/operators.py b/multi_user/operators.py
index df9853d..5360d9c 100644
--- a/multi_user/operators.py
+++ b/multi_user/operators.py
@@ -27,12 +27,12 @@ import shutil
import string
import sys
import time
+import traceback
from datetime import datetime
from operator import itemgetter
from pathlib import Path
from queue import Queue
from time import gmtime, strftime
-import traceback
from bpy.props import FloatProperty
@@ -45,16 +45,17 @@ import bpy
import mathutils
from bpy.app.handlers import persistent
from bpy_extras.io_utils import ExportHelper, ImportHelper
+from replication import porcelain
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
STATE_INITIAL, STATE_SYNCING, UP)
-from replication.protocol import DataTranslationProtocol
from replication.exception import ContextError, NonAuthorizedOperationError
from replication.interface import session
-from replication import porcelain
-from replication.repository import Repository
from replication.objects import Node
+from replication.protocol import DataTranslationProtocol
+from replication.repository import Repository
-from . import bl_types, environment, timers, ui, utils
+from . import bl_types, environment, shared_data, timers, ui, utils
+from .handlers import on_scene_update, sanitize_deps_graph
from .presence import SessionStatusWidget, renderer, view3d_find
from .timers import registry
@@ -112,7 +113,7 @@ def initialize_session():
utils.flush_history()
# Step 6: Launch deps graph update handling
- bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
+ bpy.app.handlers.depsgraph_update_post.append(on_scene_update)
@session_callback('on_exit')
@@ -132,8 +133,8 @@ def on_connection_end(reason="none"):
stop_modal_executor = True
- if depsgraph_evaluation in bpy.app.handlers.depsgraph_update_post:
- bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation)
+ if on_scene_update in bpy.app.handlers.depsgraph_update_post:
+ bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
# Step 3: remove file handled
logger = logging.getLogger()
@@ -684,6 +685,7 @@ class SessionPurgeOperator(bpy.types.Operator):
def execute(self, context):
try:
sanitize_deps_graph(remove_nodes=True)
+ porcelain.purge_orphan_nodes(session.repository)
except Exception as e:
self.report({'ERROR'}, repr(e))
@@ -716,7 +718,6 @@ class SessionNotifyOperator(bpy.types.Operator):
layout = self.layout
layout.row().label(text=self.message)
-
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -919,110 +920,6 @@ classes = (
)
-def update_external_dependencies():
- nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']]
- for node_id in nodes_ids:
- node = session.repository.graph.get(node_id)
- if node and node.owner in [session.repository.username, RP_COMMON]:
- porcelain.commit(session.repository, node_id)
- porcelain.push(session.repository,'origin', node_id)
-
-
-def sanitize_deps_graph(remove_nodes: bool = False):
- """ Cleanup the replication graph
- """
- if session and session.state == STATE_ACTIVE:
- start = utils.current_milli_time()
- rm_cpt = 0
- for node in session.repository.graph.values():
- node.instance = session.repository.rdp.resolve(node.data)
- if node is None \
- or (node.state == UP and not node.instance):
- if remove_nodes:
- try:
- porcelain.rm(session.repository,
- node.uuid,
- remove_dependencies=False)
- logging.info(f"Removing {node.uuid}")
- rm_cpt += 1
- except NonAuthorizedOperationError:
- continue
- logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
-
-
-@persistent
-def resolve_deps_graph(dummy):
- """Resolve deps graph
-
- Temporary solution to resolve each node pointers after a Undo.
- A future solution should be to avoid storing dataclock reference...
-
- """
- if session and session.state == STATE_ACTIVE:
- sanitize_deps_graph(remove_nodes=True)
-
-
-@persistent
-def load_pre_handler(dummy):
- if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
- bpy.ops.session.stop()
-
-
-@persistent
-def update_client_frame(scene):
- if session and session.state == STATE_ACTIVE:
- porcelain.update_user_metadata(session.repository, {
- 'frame_current': scene.frame_current
- })
-
-
-@persistent
-def depsgraph_evaluation(scene):
- if session and session.state == STATE_ACTIVE:
- context = bpy.context
- blender_depsgraph = bpy.context.view_layer.depsgraph
- dependency_updates = [u for u in blender_depsgraph.updates]
- settings = utils.get_preferences()
-
- update_external_dependencies()
-
- is_internal = [u for u in dependency_updates if u.is_updated_geometry or u.is_updated_shading or u.is_updated_transform]
-
- # NOTE: maybe we don't need to check each update but only the first
- if not is_internal:
- return
- for update in reversed(dependency_updates):
- # Is the object tracked ?
- if update.id.uuid:
- # Retrieve local version
- node = session.repository.graph.get(update.id.uuid)
- check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
- # Check our right on this update:
- # - if its ours or ( under common and diff), launch the
- # update process
- # - if its to someone else, ignore the update
- if node and (node.owner == session.repository.username or check_common):
- if node.state == UP:
- try:
- porcelain.commit(session.repository, node.uuid)
- porcelain.push(session.repository, 'origin', node.uuid)
- except ReferenceError:
- logging.debug(f"Reference error {node.uuid}")
- except ContextError as e:
- logging.debug(e)
- except Exception as e:
- logging.error(e)
- else:
- continue
- # A new scene is created
- elif isinstance(update.id, bpy.types.Scene):
- ref = session.repository.get_node_by_datablock(update.id)
- if ref:
- pass
- else:
- scn_uuid = porcelain.add(session.repository, update.id)
- porcelain.commit(session.node_id, scn_uuid)
- porcelain.push(session.repository,'origin', scn_uuid)
def register():
from bpy.utils import register_class
@@ -1030,13 +927,6 @@ def register():
register_class(cls)
- bpy.app.handlers.undo_post.append(resolve_deps_graph)
- bpy.app.handlers.redo_post.append(resolve_deps_graph)
-
- bpy.app.handlers.load_pre.append(load_pre_handler)
- bpy.app.handlers.frame_change_pre.append(update_client_frame)
-
-
def unregister():
if session and session.state == STATE_ACTIVE:
session.disconnect()
@@ -1044,9 +934,3 @@ def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
-
- bpy.app.handlers.undo_post.remove(resolve_deps_graph)
- bpy.app.handlers.redo_post.remove(resolve_deps_graph)
-
- bpy.app.handlers.load_pre.remove(load_pre_handler)
- bpy.app.handlers.frame_change_pre.remove(update_client_frame)
diff --git a/multi_user/shared_data.py b/multi_user/shared_data.py
new file mode 100644
index 0000000..525d9e8
--- /dev/null
+++ b/multi_user/shared_data.py
@@ -0,0 +1,48 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# ##### END GPL LICENSE BLOCK #####
+
+from replication.constants import STATE_INITIAL
+
+
+class SessionData():
+ """ A structure to share easily the current session data across the addon
+ modules.
+ This object will completely replace the Singleton lying in replication
+ interface module.
+ """
+
+ def __init__(self):
+ self.repository = None # The current repository
+ self.remote = None # The active remote
+ self.server = None
+ self.applied_updates = []
+
+ @property
+ def state(self):
+ if self.remote is None:
+ return STATE_INITIAL
+ else:
+ return self.remote.connection_status
+
+ def clear(self):
+ self.remote = None
+ self.repository = None
+ self.server = None
+ self.applied_updates = []
+
+
+session = SessionData()
diff --git a/multi_user/timers.py b/multi_user/timers.py
index 3af27a1..dafbf08 100644
--- a/multi_user/timers.py
+++ b/multi_user/timers.py
@@ -31,6 +31,8 @@ from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget,
generate_user_camera, get_view_matrix, refresh_3d_view,
refresh_sidebar_view, renderer)
+from . import shared_data
+
this = sys.modules[__name__]
# Registered timers
@@ -89,7 +91,7 @@ class Timer(object):
if bpy.app.timers.is_registered(self.main):
logging.info(f"Unregistering {self.id}")
bpy.app.timers.unregister(self.main)
-
+
del this.registry[self.id]
self.is_running = False
@@ -114,6 +116,7 @@ class ApplyTimer(Timer):
if node_ref.state == FETCHED:
try:
+ shared_data.session.applied_updates.append(node)
porcelain.apply(session.repository, node)
except Exception as e:
logging.error(f"Fail to apply {node_ref.uuid}")
@@ -251,6 +254,7 @@ class DynamicRightSelectTimer(Timer):
is_selectable = not session.repository.is_node_readonly(object_uuid)
if obj.hide_select != is_selectable:
obj.hide_select = is_selectable
+ shared_data.session.applied_updates.append(object_uuid)
class ClientUpdate(Timer):