From cd1e535a5655689b71cc21e743539c9184e60389 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 23 Dec 2020 17:27:43 +0100 Subject: [PATCH 01/37] feat: initial undo tests --- multi_user/__init__.py | 2 +- multi_user/bl_types/bl_datablock.py | 22 ++++++++++++-------- multi_user/bl_types/bl_object.py | 3 ++- multi_user/operators.py | 31 ++++++++++++++++++++++++++++- multi_user/timers.py | 2 +- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 783037b..9d8e560 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.17'), + ("replication", '0.1.18'), } diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index 836a4a2..85935cd 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -134,7 +134,7 @@ class BlDatablock(ReplicatedDatablock): else: self.diff_method = DIFF_BINARY - def resolve(self): + def resolve(self, construct = True): datablock_ref = None datablock_root = getattr(bpy.data, self.bl_id) datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root) @@ -143,14 +143,20 @@ class BlDatablock(ReplicatedDatablock): try: datablock_ref = datablock_root[self.data['name']] except Exception: - name = self.data.get('name') - logging.debug(f"Constructing {name}") - datablock_ref = self._construct(data=self.data) + if construct: + name = self.data.get('name') + logging.debug(f"Constructing {name}") + datablock_ref = self._construct(data=self.data) + for i in range(bpy.context.preferences.edit.undo_steps+1): + bpy.ops.ed.undo_push(message="Multiuser history flush") - if datablock_ref: - setattr(datablock_ref, 'uuid', self.uuid) - - self.instance = datablock_ref + if datablock_ref is not None: + setattr(datablock_ref, 'uuid', self.uuid) + self.instance = datablock_ref + return True + else: + return False + def remove_instance(self): """ diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index ca3d693..9091ff4 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -274,10 +274,11 @@ class BlObject(BlDatablock): # MODIFIERS modifiers = getattr(instance,'modifiers', None ) + data["modifiers"] = {} if modifiers: dumper.include_filter = None dumper.depth = 1 - data["modifiers"] = {} + for index, modifier in enumerate(modifiers): data["modifiers"][modifier.name] = dumper.dump(modifier) diff --git a/multi_user/operators.py b/multi_user/operators.py index 24942e2..1cae1cf 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -98,6 +98,10 @@ def initialize_session(): bpy.ops.session.apply_armature_operator('INVOKE_DEFAULT') + # Step 0: Clearing history + for i in range(bpy.context.preferences.edit.undo_steps+1): + bpy.ops.ed.undo_push(message="Multiuser history flush") + @session_callback('on_exit') def on_connection_end(reason="none"): @@ -926,8 +930,14 @@ def sanitize_deps_graph(dummy): """ if session and session.state['STATE'] == STATE_ACTIVE: + session.lock_operations() + for node_key in session.list(): - session.get(node_key).resolve() + node = session.get(node_key) + node.resolve(construct=False) + + session.unlock_operations() + @persistent @@ -978,6 +988,16 @@ def depsgraph_evaluation(scene): # # New items ! # logger.error("UPDATE: ADD") +@persistent +def unlock(dummy): + if session and session.state['STATE'] == STATE_ACTIVE: + session.unlock_operations() + +@persistent +def lock(dummy): + if session and session.state['STATE'] == STATE_ACTIVE: + session.lock_operations() + def register(): from bpy.utils import register_class @@ -985,6 +1005,11 @@ def register(): for cls in classes: register_class(cls) + bpy.app.handlers.undo_post.append(unlock) + bpy.app.handlers.undo_pre.append(lock) + bpy.app.handlers.redo_pre.append(unlock) + bpy.app.handlers.redo_post.append(lock) + bpy.app.handlers.undo_post.append(sanitize_deps_graph) bpy.app.handlers.redo_post.append(sanitize_deps_graph) @@ -1000,6 +1025,10 @@ def unregister(): for cls in reversed(classes): unregister_class(cls) + bpy.app.handlers.undo_post.remove(unlock) + bpy.app.handlers.undo_pre.remove(lock) + bpy.app.handlers.redo_pre.remove(unlock) + bpy.app.handlers.redo_post.remove(lock) bpy.app.handlers.undo_post.remove(sanitize_deps_graph) bpy.app.handlers.redo_post.remove(sanitize_deps_graph) diff --git a/multi_user/timers.py b/multi_user/timers.py index 9fd6dda..b9f45d4 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -104,7 +104,7 @@ class ApplyTimer(Timer): def __init__(self, timeout=1, target_type=None): self._type = target_type super().__init__(timeout) - self.id = target_type.__name__ + self.id = target_type.__name__ if target_type else "ApplyTimer" def execute(self): if session and session.state['STATE'] == STATE_ACTIVE: From 2f4e30f432b1cabe759b5dc4731220940b59843a Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 23 Dec 2020 17:57:36 +0100 Subject: [PATCH 02/37] fix: catch runtime error feat: enable build for undo branch --- .gitlab/ci/build.gitlab-ci.yml | 3 ++- multi_user/bl_types/bl_datablock.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index 3d60f6d..aee8ffd 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -10,4 +10,5 @@ build: only: refs: - master - - develop \ No newline at end of file + - develop + - 132-fix-undo-edit-last-operation-redo-handling-2 \ No newline at end of file diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index 85935cd..d2e36fd 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -147,8 +147,12 @@ class BlDatablock(ReplicatedDatablock): name = self.data.get('name') logging.debug(f"Constructing {name}") datablock_ref = self._construct(data=self.data) - for i in range(bpy.context.preferences.edit.undo_steps+1): - bpy.ops.ed.undo_push(message="Multiuser history flush") + + try: + for i in range(bpy.context.preferences.edit.undo_steps+1): + bpy.ops.ed.undo_push(message="Multiuser history flush") + except RuntimeError: + logging.error("Fail to overwrite history") if datablock_ref is not None: setattr(datablock_ref, 'uuid', self.uuid) From 3d9da73ab09b071b8d6d806483e37ecf02fd0376 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 23 Dec 2020 18:46:29 +0100 Subject: [PATCH 03/37] feat: flush history on collection and scene update only feat: enable deploy for undo branch --- .gitlab/ci/deploy.gitlab-ci.yml | 3 ++- multi_user/__init__.py | 2 +- multi_user/bl_types/bl_collection.py | 4 ++++ multi_user/bl_types/bl_datablock.py | 6 ------ multi_user/bl_types/bl_scene.py | 5 +++++ multi_user/operators.py | 3 +-- multi_user/utils.py | 9 +++++++++ 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.gitlab/ci/deploy.gitlab-ci.yml b/.gitlab/ci/deploy.gitlab-ci.yml index a5e9778..580bd05 100644 --- a/.gitlab/ci/deploy.gitlab-ci.yml +++ b/.gitlab/ci/deploy.gitlab-ci.yml @@ -21,4 +21,5 @@ deploy: only: refs: - master - - develop \ No newline at end of file + - develop + - 132-fix-undo-edit-last-operation-redo-handling-2 \ No newline at end of file diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 9d8e560..7410d83 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -19,7 +19,7 @@ bl_info = { "name": "Multi-User", "author": "Swann Martinez", - "version": (0, 2, 0), + "version": (0, 3, 0), "description": "Enable real-time collaborative workflow inside blender", "blender": (2, 82, 0), "location": "3D View > Sidebar > Multi-User tab", diff --git a/multi_user/bl_types/bl_collection.py b/multi_user/bl_types/bl_collection.py index 7b78989..c9874e4 100644 --- a/multi_user/bl_types/bl_collection.py +++ b/multi_user/bl_types/bl_collection.py @@ -114,6 +114,10 @@ class BlCollection(BlDatablock): # Link childrens load_collection_childrens(data['children'], target) + # FIXME: Find a better way after the replication big refacotoring + # Keep other user from deleting collection object by flushing their history + utils.flush_history() + def _dump_implementation(self, data, instance=None): assert(instance) diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index d2e36fd..d10cbe4 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -148,12 +148,6 @@ class BlDatablock(ReplicatedDatablock): logging.debug(f"Constructing {name}") datablock_ref = self._construct(data=self.data) - try: - for i in range(bpy.context.preferences.edit.undo_steps+1): - bpy.ops.ed.undo_push(message="Multiuser history flush") - except RuntimeError: - logging.error("Fail to overwrite history") - if datablock_ref is not None: setattr(datablock_ref, 'uuid', self.uuid) self.instance = datablock_ref diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index 575f515..df7e9a6 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -28,6 +28,7 @@ from .bl_collection import (dump_collection_children, dump_collection_objects, resolve_collection_dependencies) from .bl_datablock import BlDatablock from .dump_anything import Dumper, Loader +from ..utils import flush_history RENDER_SETTINGS = [ 'dither_intensity', @@ -328,6 +329,10 @@ class BlScene(BlDatablock): 'view_settings']['curve_mapping']['black_level'] target.view_settings.curve_mapping.update() + # FIXME: Find a better way after the replication big refacotoring + # Keep other user from deleting collection object by flushing their history + flush_history() + def _dump_implementation(self, data, instance=None): assert(instance) diff --git a/multi_user/operators.py b/multi_user/operators.py index 1cae1cf..942aea0 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -99,8 +99,7 @@ def initialize_session(): bpy.ops.session.apply_armature_operator('INVOKE_DEFAULT') # Step 0: Clearing history - for i in range(bpy.context.preferences.edit.undo_steps+1): - bpy.ops.ed.undo_push(message="Multiuser history flush") + utils.flush_history() @session_callback('on_exit') diff --git a/multi_user/utils.py b/multi_user/utils.py index 57ed532..8667b2d 100644 --- a/multi_user/utils.py +++ b/multi_user/utils.py @@ -65,6 +65,15 @@ def get_datablock_users(datablock): return users +def flush_history(): + try: + logging.info("Flushing history") + for i in range(bpy.context.preferences.edit.undo_steps+1): + bpy.ops.ed.undo_push(message="Multiuser history flush") + except RuntimeError: + logging.error("Fail to overwrite history") + + def get_state_str(state): state_str = 'UNKOWN' if state == STATE_WAITING: From 4b1499f6ae0c93075a4b7da9864c1c715c4ab191 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 29 Dec 2020 17:38:21 +0100 Subject: [PATCH 04/37] fix: construct error --- multi_user/bl_types/bl_file.py | 2 +- multi_user/bl_types/bl_sequencer.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/multi_user/bl_types/bl_file.py b/multi_user/bl_types/bl_file.py index 1dd8919..067dd2a 100644 --- a/multi_user/bl_types/bl_file.py +++ b/multi_user/bl_types/bl_file.py @@ -71,7 +71,7 @@ class BlFile(ReplicatedDatablock): self.preferences = utils.get_preferences() self.diff_method = DIFF_BINARY - def resolve(self): + def resolve(self, construct = True): if self.data: self.instance = Path(get_filepath(self.data['name'])) diff --git a/multi_user/bl_types/bl_sequencer.py b/multi_user/bl_types/bl_sequencer.py index 6d74aa3..d961ddc 100644 --- a/multi_user/bl_types/bl_sequencer.py +++ b/multi_user/bl_types/bl_sequencer.py @@ -143,10 +143,10 @@ class BlSequencer(BlDatablock): return scene.sequence_editor - def resolve(self): + def resolve(self, construct = True): scene = bpy.data.scenes.get(self.data['name'], None) if scene: - if scene.sequence_editor is None: + if scene.sequence_editor is None and construct: self.instance = self._construct(self.data) else: self.instance = scene.sequence_editor From ca2d8e49b529b1014517fb098b46c578a268f841 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 6 Jan 2021 13:58:58 +0100 Subject: [PATCH 05/37] feat: update replication --- multi_user/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 7410d83..c1bf38c 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.18'), + ("replication", '0.1.19'), } From c24f70fad5ab35bf2fd962285420a5aa8fb2e63f Mon Sep 17 00:00:00 2001 From: Swann Date: Sat, 9 Jan 2021 22:36:00 +0100 Subject: [PATCH 06/37] feat: force to use depsgraph update mode --- multi_user/__init__.py | 2 +- multi_user/bl_types/bl_datablock.py | 19 ++++++----- multi_user/operators.py | 51 ++++++++++++++++------------- multi_user/preferences.py | 12 ------- multi_user/ui.py | 28 ---------------- 5 files changed, 40 insertions(+), 72 deletions(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index c1bf38c..0f862d2 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.19'), + ("replication", '0.1.20'), } diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index c016b4f..b33fe5d 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -137,16 +137,19 @@ class BlDatablock(ReplicatedDatablock): def resolve(self, construct = True): datablock_ref = None datablock_root = getattr(bpy.data, self.bl_id) - datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root) + + try: + datablock_ref = datablock_root[self.data['name']] + except Exception: + pass if not datablock_ref: - try: - datablock_ref = datablock_root[self.data['name']] - except Exception: - if construct: - name = self.data.get('name') - logging.debug(f"Constructing {name}") - datablock_ref = self._construct(data=self.data) + datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root) + + if construct and not datablock_ref: + name = self.data.get('name') + logging.debug(f"Constructing {name}") + datablock_ref = self._construct(data=self.data) if datablock_ref is not None: setattr(datablock_ref, 'uuid', self.uuid) diff --git a/multi_user/operators.py b/multi_user/operators.py index 942aea0..2f553f2 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -45,7 +45,7 @@ from bpy_extras.io_utils import ExportHelper, ImportHelper from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE, STATE_INITIAL, STATE_SYNCING, UP) from replication.data import ReplicatedDataFactory -from replication.exception import NonAuthorizedOperationError +from replication.exception import NonAuthorizedOperationError, ContextError from replication.interface import session from . import bl_types, environment, timers, ui, utils @@ -93,8 +93,7 @@ def initialize_session(): for d in deleyables: d.register() - if settings.update_method == 'DEPSGRAPH': - bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation) + bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation) bpy.ops.session.apply_armature_operator('INVOKE_DEFAULT') @@ -119,9 +118,7 @@ def on_connection_end(reason="none"): stop_modal_executor = True - if settings.update_method == 'DEPSGRAPH': - bpy.app.handlers.depsgraph_update_post.remove( - depsgraph_evaluation) + bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation) # Step 3: remove file handled logger = logging.getLogger() @@ -151,7 +148,7 @@ class SessionStartOperator(bpy.types.Operator): runtime_settings = context.window_manager.session users = bpy.data.window_managers['WinMan'].online_users admin_pass = runtime_settings.password - use_extern_update = settings.update_method == 'DEPSGRAPH' + users.clear() deleyables.clear() @@ -203,12 +200,11 @@ class SessionStartOperator(bpy.types.Operator): automatic=type_local_config.auto_push, check_common=type_module_class.bl_check_common) - if settings.update_method == 'DEFAULT': - if type_local_config.bl_delay_apply > 0: - deleyables.append( - timers.ApplyTimer( - timeout=type_local_config.bl_delay_apply, - target_type=type_module_class)) + if type_local_config.bl_delay_apply > 0: + deleyables.append( + timers.ApplyTimer( + timeout=type_local_config.bl_delay_apply, + target_type=type_module_class)) if bpy.app.version[1] >= 91: python_binary_path = sys.executable @@ -218,11 +214,7 @@ class SessionStartOperator(bpy.types.Operator): session.configure( factory=bpy_factory, python_path=python_binary_path, - external_update_handling=use_extern_update) - - if settings.update_method == 'DEPSGRAPH': - deleyables.append(timers.ApplyTimer( - settings.depsgraph_update_rate/1000)) + external_update_handling=True) # Host a session if self.host: @@ -929,13 +921,14 @@ def sanitize_deps_graph(dummy): """ if session and session.state['STATE'] == STATE_ACTIVE: - session.lock_operations() - + # pass + # session.lock_operations() + start = utils.current_milli_time() for node_key in session.list(): node = session.get(node_key) node.resolve(construct=False) - - session.unlock_operations() + logging.debug(f"Sanitize took { utils.current_milli_time()-start}ms") + # session.unlock_operations() @@ -979,7 +972,19 @@ def depsgraph_evaluation(scene): not settings.sync_flags.sync_during_editmode: break - session.stash(node.uuid) + # session.stash(node.uuid) + if node.has_changed(): + try: + session.commit(node.uuid) + session.push(node.uuid) + except ReferenceError: + logging.debug(f"Reference error {node.uuid}") + if not node.is_valid(): + session.remove(node.uuid) + except ContextError as e: + logging.debug(e) + except Exception as e: + logging.error(e) else: # Distant update continue diff --git a/multi_user/preferences.py b/multi_user/preferences.py index 3a2d816..c248e89 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -199,15 +199,6 @@ class SessionPrefs(bpy.types.AddonPreferences): description='connection timeout before disconnection', default=1000 ) - update_method: bpy.props.EnumProperty( - name='update method', - description='replication update method', - items=[ - ('DEFAULT', "Default", "Default: Use threads to monitor databloc changes"), - ('DEPSGRAPH', "Depsgraph", - "Experimental: Use the blender dependency graph to trigger updates"), - ], - ) # Replication update settings depsgraph_update_rate: bpy.props.IntProperty( name='depsgraph update rate', @@ -390,9 +381,6 @@ class SessionPrefs(bpy.types.AddonPreferences): row = box.row() row.label(text="Init the session from:") row.prop(self, "init_method", text="") - row = box.row() - row.label(text="Update method:") - row.prop(self, "update_method", text="") table = box.box() table.row().prop( diff --git a/multi_user/ui.py b/multi_user/ui.py index bca2e0e..bf44a67 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -269,7 +269,6 @@ class SESSION_PT_advanced_settings(bpy.types.Panel): if settings.sidebar_advanced_rep_expanded: replication_section_row = replication_section.row() - replication_section_row.label(text="Sync flags", icon='COLLECTION_NEW') replication_section_row = replication_section.row() replication_section_row.prop(settings.sync_flags, "sync_render_settings") replication_section_row = replication_section.row() @@ -283,33 +282,6 @@ class SESSION_PT_advanced_settings(bpy.types.Panel): warning.label(text="Don't use this with heavy meshes !", icon='ERROR') replication_section_row = replication_section.row() - replication_section_row.label(text="Update method", icon='RECOVER_LAST') - replication_section_row = replication_section.row() - replication_section_row.prop(settings, "update_method", expand=True) - replication_section_row = replication_section.row() - replication_timers = replication_section_row.box() - replication_timers.label(text="Replication timers", icon='TIME') - if settings.update_method == "DEFAULT": - replication_timers = replication_timers.row() - # Replication frequencies - flow = replication_timers.grid_flow( - row_major=True, columns=0, even_columns=True, even_rows=False, align=True) - line = flow.row(align=True) - line.label(text=" ") - line.separator() - line.label(text="refresh (sec)") - line.label(text="apply (sec)") - - for item in settings.supported_datablocks: - line = flow.row(align=True) - line.prop(item, "auto_push", text="", icon=item.icon) - line.separator() - line.prop(item, "bl_delay_refresh", text="") - line.prop(item, "bl_delay_apply", text="") - else: - replication_timers = replication_timers.row() - replication_timers.label(text="Update rate (ms):") - replication_timers.prop(settings, "depsgraph_update_rate", text="") cache_section = layout.row().box() cache_section.prop( From 12bd4a603b5ad389158280523a4dd7cd2e961548 Mon Sep 17 00:00:00 2001 From: Swann Date: Mon, 11 Jan 2021 19:54:57 +0100 Subject: [PATCH 07/37] feat: added a push timer to control the push frequency --- multi_user/__init__.py | 2 +- multi_user/operators.py | 61 +++++++++++------------------------------ multi_user/timers.py | 28 ++++++++++++++++++- 3 files changed, 44 insertions(+), 47 deletions(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 0f862d2..efa18cd 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.20'), + ("replication", '0.1.21'), } diff --git a/multi_user/operators.py b/multi_user/operators.py index 2f553f2..040fc5b 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -53,6 +53,8 @@ from .presence import SessionStatusWidget, renderer, view3d_find from .timers import registry background_execution_queue = Queue() +stagging = list() + deleyables = [] stop_modal_executor = False @@ -79,13 +81,13 @@ def initialize_session(): # Step 1: Constrect nodes for node in session._graph.list_ordered(): - node_ref = session.get(node) + node_ref = session.get(uuid=node) if node_ref.state == FETCHED: node_ref.resolve() # Step 2: Load nodes for node in session._graph.list_ordered(): - node_ref = session.get(node) + node_ref = session.get(uuid=node) if node_ref.state == FETCHED: node_ref.apply() @@ -118,7 +120,8 @@ def on_connection_end(reason="none"): stop_modal_executor = True - bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation) + if depsgraph_evaluation in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation) # Step 3: remove file handled logger = logging.getLogger() @@ -266,7 +269,7 @@ class SessionStartOperator(bpy.types.Operator): # Background client updates service deleyables.append(timers.ClientUpdate()) deleyables.append(timers.DynamicRightSelectTimer()) - + deleyables.append(timers.PushTimer(queue=stagging)) session_update = timers.SessionStatusUpdate() session_user_sync = timers.SessionUserSync() session_background_executor = timers.MainThreadExecutor( @@ -921,14 +924,11 @@ def sanitize_deps_graph(dummy): """ if session and session.state['STATE'] == STATE_ACTIVE: - # pass - # session.lock_operations() start = utils.current_milli_time() for node_key in session.list(): node = session.get(node_key) node.resolve(construct=False) logging.debug(f"Sanitize took { utils.current_milli_time()-start}ms") - # session.unlock_operations() @@ -960,31 +960,21 @@ def depsgraph_evaluation(scene): # Is the object tracked ? if update.id.uuid: # Retrieve local version - node = session.get(update.id.uuid) + node = session.get(uuid=update.id.uuid) # 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 (go deeper ?) - if node and node.owner in [session.id, RP_COMMON] and node.state == UP: - # Avoid slow geometry update - if 'EDIT' in context.mode and \ - not settings.sync_flags.sync_during_editmode: - break + if node and node.owner in [session.id, RP_COMMON]: + if node.state == UP: + # Avoid slow geometry update + if 'EDIT' in context.mode and \ + not settings.sync_flags.sync_during_editmode: + break - # session.stash(node.uuid) - if node.has_changed(): - try: - session.commit(node.uuid) - session.push(node.uuid) - except ReferenceError: - logging.debug(f"Reference error {node.uuid}") - if not node.is_valid(): - session.remove(node.uuid) - except ContextError as e: - logging.debug(e) - except Exception as e: - logging.error(e) + if node.uuid not in stagging: + stagging.append(node.uuid) else: # Distant update continue @@ -992,16 +982,6 @@ def depsgraph_evaluation(scene): # # New items ! # logger.error("UPDATE: ADD") -@persistent -def unlock(dummy): - if session and session.state['STATE'] == STATE_ACTIVE: - session.unlock_operations() - -@persistent -def lock(dummy): - if session and session.state['STATE'] == STATE_ACTIVE: - session.lock_operations() - def register(): from bpy.utils import register_class @@ -1009,11 +989,6 @@ def register(): for cls in classes: register_class(cls) - bpy.app.handlers.undo_post.append(unlock) - bpy.app.handlers.undo_pre.append(lock) - bpy.app.handlers.redo_pre.append(unlock) - bpy.app.handlers.redo_post.append(lock) - bpy.app.handlers.undo_post.append(sanitize_deps_graph) bpy.app.handlers.redo_post.append(sanitize_deps_graph) @@ -1029,10 +1004,6 @@ def unregister(): for cls in reversed(classes): unregister_class(cls) - bpy.app.handlers.undo_post.remove(unlock) - bpy.app.handlers.undo_pre.remove(lock) - bpy.app.handlers.redo_pre.remove(unlock) - bpy.app.handlers.redo_post.remove(lock) bpy.app.handlers.undo_post.remove(sanitize_deps_graph) bpy.app.handlers.redo_post.remove(sanitize_deps_graph) diff --git a/multi_user/timers.py b/multi_user/timers.py index b9f45d4..ab70bfb 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -22,7 +22,7 @@ import bpy from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE, STATE_INITIAL, STATE_LOBBY, STATE_QUITTING, STATE_SRV_SYNC, STATE_SYNCING, UP) -from replication.exception import NonAuthorizedOperationError +from replication.exception import NonAuthorizedOperationError, ContextError from replication.interface import session from . import operators, utils @@ -130,6 +130,32 @@ class ApplyTimer(Timer): if deps and node in deps: session.apply(n, force=True) +class PushTimer(Timer): + def __init__(self, timeout=1, queue=None): + super().__init__(timeout) + self.id = "PushTimer" + self.q_push = queue + + def execute(self): + while self.q_push: + node_id = self.q_push.pop() + + node = session.get(uuid=node_id) + if node.has_changed(): + try: + session.commit(node.uuid) + session.push(node.uuid) + except ReferenceError: + logging.debug(f"Reference error {node.uuid}") + if not node.is_valid(): + session.remove(node.uuid) + except ContextError as e: + logging.debug(e) + except Exception as e: + logging.error(e) + else: + logging.info("Skipping updatem no changes") + class DynamicRightSelectTimer(Timer): def __init__(self, timeout=.1): super().__init__(timeout) From e3bd7ea445acb2ab919b49a26ce8db02642572c4 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 12 Jan 2021 10:29:27 +0100 Subject: [PATCH 08/37] feat: improve commit times by using cache from the diff --- multi_user/__init__.py | 2 +- multi_user/timers.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index efa18cd..1bb10b1 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.21'), + ("replication", '0.1.22'), } diff --git a/multi_user/timers.py b/multi_user/timers.py index ab70bfb..acf6c10 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -138,13 +138,13 @@ class PushTimer(Timer): def execute(self): while self.q_push: - node_id = self.q_push.pop() - - node = session.get(uuid=node_id) + node = session.get(uuid= self.q_push.pop()) + start = utils.current_milli_time() + if node.has_changed(): try: session.commit(node.uuid) - session.push(node.uuid) + session.push(node.uuid, check_data=False) except ReferenceError: logging.debug(f"Reference error {node.uuid}") if not node.is_valid(): From abd846fc8d3f4fa9b0960d4c26aa48e3fe15b2d7 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 12 Jan 2021 11:33:48 +0100 Subject: [PATCH 09/37] clean: push timer feat: purge stagging on pre--redo/undo --- multi_user/operators.py | 15 +++++++++++++-- multi_user/preferences.py | 8 ++++---- multi_user/timers.py | 3 +-- multi_user/ui.py | 1 + 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index 040fc5b..72b31ff 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -54,7 +54,7 @@ from .timers import registry background_execution_queue = Queue() stagging = list() - +locking = False deleyables = [] stop_modal_executor = False @@ -269,7 +269,10 @@ class SessionStartOperator(bpy.types.Operator): # Background client updates service deleyables.append(timers.ClientUpdate()) deleyables.append(timers.DynamicRightSelectTimer()) - deleyables.append(timers.PushTimer(queue=stagging)) + deleyables.append(timers.PushTimer( + queue=stagging, + timeout=settings.depsgraph_update_rate + )) session_update = timers.SessionStatusUpdate() session_user_sync = timers.SessionUserSync() session_background_executor = timers.MainThreadExecutor( @@ -982,6 +985,8 @@ def depsgraph_evaluation(scene): # # New items ! # logger.error("UPDATE: ADD") +def clear_staging(dummy): + stagging.clear() def register(): from bpy.utils import register_class @@ -989,6 +994,9 @@ def register(): for cls in classes: register_class(cls) + bpy.app.handlers.undo_pre.append(clear_staging) + bpy.app.handlers.redo_pre.append(clear_staging) + bpy.app.handlers.undo_post.append(sanitize_deps_graph) bpy.app.handlers.redo_post.append(sanitize_deps_graph) @@ -1004,6 +1012,9 @@ def unregister(): for cls in reversed(classes): unregister_class(cls) + bpy.app.handlers.undo_pre.remove(clear_staging) + bpy.app.handlers.redo_pre.remove(clear_staging) + bpy.app.handlers.undo_post.remove(sanitize_deps_graph) bpy.app.handlers.redo_post.remove(sanitize_deps_graph) diff --git a/multi_user/preferences.py b/multi_user/preferences.py index c248e89..e40fc9e 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -200,10 +200,10 @@ class SessionPrefs(bpy.types.AddonPreferences): default=1000 ) # Replication update settings - depsgraph_update_rate: bpy.props.IntProperty( - name='depsgraph update rate', - description='Dependency graph uppdate rate (milliseconds)', - default=1000 + depsgraph_update_rate: bpy.props.FloatProperty( + name='depsgraph update rate (s)', + description='Dependency graph uppdate rate (s)', + default=1 ) clear_memory_filecache: bpy.props.BoolProperty( name="Clear memory filecache", diff --git a/multi_user/timers.py b/multi_user/timers.py index acf6c10..3042e73 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -139,7 +139,6 @@ class PushTimer(Timer): def execute(self): while self.q_push: node = session.get(uuid= self.q_push.pop()) - start = utils.current_milli_time() if node.has_changed(): try: @@ -154,7 +153,7 @@ class PushTimer(Timer): except Exception as e: logging.error(e) else: - logging.info("Skipping updatem no changes") + logging.debug("Skipping update no changes") class DynamicRightSelectTimer(Timer): def __init__(self, timeout=.1): diff --git a/multi_user/ui.py b/multi_user/ui.py index bf44a67..ea575a6 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -281,6 +281,7 @@ class SESSION_PT_advanced_settings(bpy.types.Panel): warning = replication_section_row.box() warning.label(text="Don't use this with heavy meshes !", icon='ERROR') replication_section_row = replication_section.row() + replication_section_row.prop(settings, "depsgraph_update_rate") cache_section = layout.row().box() From d3211199c4d791b601505b9e895e8de385985283 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 12 Jan 2021 13:36:21 +0100 Subject: [PATCH 10/37] fix: handler not correctly removed --- multi_user/operators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/multi_user/operators.py b/multi_user/operators.py index 72b31ff..78356b2 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -985,6 +985,7 @@ def depsgraph_evaluation(scene): # # New items ! # logger.error("UPDATE: ADD") +@persistent def clear_staging(dummy): stagging.clear() From f5c77fec3ab034bbb7a7ae0e6dad1cc3fe6c03a7 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 12 Jan 2021 21:29:36 +0100 Subject: [PATCH 11/37] fix: fail to push collectionsby reverting the commit caching policy --- multi_user/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 1bb10b1..2cfe4a1 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.22'), + ("replication", '0.1.23'), } From 2bde136bb6a69b56f6cf77191a96ad0c2ba545ec Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 13 Jan 2021 14:24:16 +0100 Subject: [PATCH 12/37] fix: annotation tool --- multi_user/timers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/multi_user/timers.py b/multi_user/timers.py index 3042e73..5d100ed 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -174,6 +174,9 @@ class DynamicRightSelectTimer(Timer): ctx = bpy.context annotation_gp = ctx.scene.grease_pencil + if annotation_gp and not annotation_gp.uuid: + ctx.scene.update_tag() + # if an annotation exist and is tracked if annotation_gp and annotation_gp.uuid: registered_gp = session.get(uuid=annotation_gp.uuid) @@ -188,6 +191,13 @@ class DynamicRightSelectTimer(Timer): settings.username, ignore_warnings=True, affect_dependencies=False) + + if registered_gp.owner == settings.username: + gp_node = session.get(uuid=annotation_gp.uuid) + if gp_node.has_changed(): + session.commit(gp_node.uuid) + session.push(gp_node.uuid, check_data=False) + elif self._annotating: session.change_owner( registered_gp.uuid, From 6a00b586007aa60f5d281d0edeff687a3dbfb54d Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 13 Jan 2021 14:45:23 +0100 Subject: [PATCH 13/37] fix: timeout error on connection --- multi_user/operators.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index 78356b2..c784a18 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -95,13 +95,14 @@ def initialize_session(): for d in deleyables: d.register() - bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation) - bpy.ops.session.apply_armature_operator('INVOKE_DEFAULT') - # Step 0: Clearing history + # Step 5: Clearing history utils.flush_history() + # Step 6: Launch deps graph update handling + bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation) + @session_callback('on_exit') def on_connection_end(reason="none"): @@ -958,17 +959,24 @@ def depsgraph_evaluation(scene): settings = utils.get_preferences() # NOTE: maybe we don't need to check each update but only the first - for update in reversed(dependency_updates): # Is the object tracked ? if update.id.uuid: # Retrieve local version node = session.get(uuid=update.id.uuid) + # Check if the update needs to be replicated else ignore it + if update.is_updated_transform \ + or update.is_updated_shading \ + or update.is_updated_geometry \ + or isinstance(update.id,bpy.types.Collection): + else: + continue + # 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 (go deeper ?) + # - if its to someone else, ignore the update if node and node.owner in [session.id, RP_COMMON]: if node.state == UP: # Avoid slow geometry update @@ -979,11 +987,7 @@ def depsgraph_evaluation(scene): if node.uuid not in stagging: stagging.append(node.uuid) else: - # Distant update continue - # else: - # # New items ! - # logger.error("UPDATE: ADD") @persistent def clear_staging(dummy): From 2913e6d5a758c0102447812763ec37e09c9a4595 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 13 Jan 2021 15:09:10 +0100 Subject: [PATCH 14/37] fix: file push --- multi_user/bl_types/bl_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/multi_user/bl_types/bl_file.py b/multi_user/bl_types/bl_file.py index 067dd2a..c4e7946 100644 --- a/multi_user/bl_types/bl_file.py +++ b/multi_user/bl_types/bl_file.py @@ -79,8 +79,8 @@ class BlFile(ReplicatedDatablock): logging.debug("File don't exist, loading it.") self._load(self.data, self.instance) - def push(self, socket, identity=None): - super().push(socket, identity=None) + def push(self, socket, identity=None, check_data=False): + super().push(socket, identity=None, check_data=False) if self.preferences.clear_memory_filecache: del self.data['file'] From 52ebb874b0a12996b922f3421c112346786b0dcf Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 13 Jan 2021 15:36:41 +0100 Subject: [PATCH 15/37] fix: edit-mode error while pushing --- multi_user/operators.py | 12 ++++-------- multi_user/timers.py | 23 ++++++++++------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index c784a18..db60ca7 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -76,21 +76,25 @@ def session_callback(name): def initialize_session(): """Session connection init hander """ + logging.info("Intializing the scene") settings = utils.get_preferences() runtime_settings = bpy.context.window_manager.session + logging.info("Constructing nodes") # Step 1: Constrect nodes for node in session._graph.list_ordered(): node_ref = session.get(uuid=node) if node_ref.state == FETCHED: node_ref.resolve() + logging.info("Loading nodes") # Step 2: Load nodes for node in session._graph.list_ordered(): node_ref = session.get(uuid=node) if node_ref.state == FETCHED: node_ref.apply() + logging.info("Registering timers") # Step 4: Register blender timers for d in deleyables: d.register() @@ -965,14 +969,6 @@ def depsgraph_evaluation(scene): # Retrieve local version node = session.get(uuid=update.id.uuid) - # Check if the update needs to be replicated else ignore it - if update.is_updated_transform \ - or update.is_updated_shading \ - or update.is_updated_geometry \ - or isinstance(update.id,bpy.types.Collection): - else: - continue - # Check our right on this update: # - if its ours or ( under common and diff), launch the # update process diff --git a/multi_user/timers.py b/multi_user/timers.py index 5d100ed..ff17505 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -140,21 +140,18 @@ class PushTimer(Timer): while self.q_push: node = session.get(uuid= self.q_push.pop()) - if node.has_changed(): - try: + try: + if node.has_changed(): session.commit(node.uuid) session.push(node.uuid, check_data=False) - except ReferenceError: - logging.debug(f"Reference error {node.uuid}") - if not node.is_valid(): - session.remove(node.uuid) - except ContextError as e: - logging.debug(e) - except Exception as e: - logging.error(e) - else: - logging.debug("Skipping update no changes") - + except ReferenceError: + logging.debug(f"Reference error {node.uuid}") + if not node.is_valid(): + session.remove(node.uuid) + except ContextError as e: + logging.debug(e) + except Exception as e: + logging.error(e) class DynamicRightSelectTimer(Timer): def __init__(self, timeout=.1): super().__init__(timeout) From 89a88910735f7dceb3275bca982621f1817e697d Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 13 Jan 2021 15:49:07 +0100 Subject: [PATCH 16/37] fix: RNA removed error by disabling the push timer --- multi_user/operators.py | 22 ++++++++++++++++------ multi_user/timers.py | 1 + 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index db60ca7..80ebc43 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -274,10 +274,10 @@ class SessionStartOperator(bpy.types.Operator): # Background client updates service deleyables.append(timers.ClientUpdate()) deleyables.append(timers.DynamicRightSelectTimer()) - deleyables.append(timers.PushTimer( - queue=stagging, - timeout=settings.depsgraph_update_rate - )) + # deleyables.append(timers.PushTimer( + # queue=stagging, + # timeout=settings.depsgraph_update_rate + # )) session_update = timers.SessionStatusUpdate() session_user_sync = timers.SessionUserSync() session_background_executor = timers.MainThreadExecutor( @@ -980,8 +980,18 @@ def depsgraph_evaluation(scene): not settings.sync_flags.sync_during_editmode: break - if node.uuid not in stagging: - stagging.append(node.uuid) + try: + if node.has_changed(): + session.commit(node.uuid) + session.push(node.uuid, check_data=False) + except ReferenceError: + logging.debug(f"Reference error {node.uuid}") + if not node.is_valid(): + session.remove(node.uuid) + except ContextError as e: + logging.debug(e) + except Exception as e: + logging.error(e) else: continue diff --git a/multi_user/timers.py b/multi_user/timers.py index ff17505..5867177 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -152,6 +152,7 @@ class PushTimer(Timer): logging.debug(e) except Exception as e: logging.error(e) + class DynamicRightSelectTimer(Timer): def __init__(self, timeout=.1): super().__init__(timeout) From a2124459273810cd30ea87f7ee2fb113f2bee809 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 15 Jan 2021 16:41:32 +0100 Subject: [PATCH 17/37] clean: remove push timer --- multi_user/operators.py | 10 ---------- multi_user/timers.py | 22 ---------------------- 2 files changed, 32 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index 80ebc43..830a23c 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -53,8 +53,6 @@ from .presence import SessionStatusWidget, renderer, view3d_find from .timers import registry background_execution_queue = Queue() -stagging = list() -locking = False deleyables = [] stop_modal_executor = False @@ -995,9 +993,6 @@ def depsgraph_evaluation(scene): else: continue -@persistent -def clear_staging(dummy): - stagging.clear() def register(): from bpy.utils import register_class @@ -1005,8 +1000,6 @@ def register(): for cls in classes: register_class(cls) - bpy.app.handlers.undo_pre.append(clear_staging) - bpy.app.handlers.redo_pre.append(clear_staging) bpy.app.handlers.undo_post.append(sanitize_deps_graph) bpy.app.handlers.redo_post.append(sanitize_deps_graph) @@ -1023,9 +1016,6 @@ def unregister(): for cls in reversed(classes): unregister_class(cls) - bpy.app.handlers.undo_pre.remove(clear_staging) - bpy.app.handlers.redo_pre.remove(clear_staging) - bpy.app.handlers.undo_post.remove(sanitize_deps_graph) bpy.app.handlers.redo_post.remove(sanitize_deps_graph) diff --git a/multi_user/timers.py b/multi_user/timers.py index 5867177..62fdeee 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -130,28 +130,6 @@ class ApplyTimer(Timer): if deps and node in deps: session.apply(n, force=True) -class PushTimer(Timer): - def __init__(self, timeout=1, queue=None): - super().__init__(timeout) - self.id = "PushTimer" - self.q_push = queue - - def execute(self): - while self.q_push: - node = session.get(uuid= self.q_push.pop()) - - try: - if node.has_changed(): - session.commit(node.uuid) - session.push(node.uuid, check_data=False) - except ReferenceError: - logging.debug(f"Reference error {node.uuid}") - if not node.is_valid(): - session.remove(node.uuid) - except ContextError as e: - logging.debug(e) - except Exception as e: - logging.error(e) class DynamicRightSelectTimer(Timer): def __init__(self, timeout=.1): From f4463f9cfe66d94a564e3408a6a505d53fd57d9e Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 15 Jan 2021 16:54:36 +0100 Subject: [PATCH 18/37] fix: greace pencil object vertexgroup dump crash by adding a warning --- multi_user/bl_types/bl_object.py | 39 +++++++++++++++++--------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 2aa21e6..182ae2f 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -376,29 +376,32 @@ class BlObject(BlDatablock): # VERTEx GROUP if len(instance.vertex_groups) > 0: - points_attr = 'vertices' if isinstance( - instance.data, bpy.types.Mesh) else 'points' - vg_data = [] - for vg in instance.vertex_groups: - vg_idx = vg.index - dumped_vg = {} - dumped_vg['name'] = vg.name + if isinstance( instance.data, bpy.types.GreasePencil): + logging.warning("Grease pencil vertex groups are not supported yet. More info: https://gitlab.com/slumber/multi-user/-/issues/161") + else: + points_attr = 'vertices' if isinstance( + instance.data, bpy.types.Mesh) else 'points' + vg_data = [] + for vg in instance.vertex_groups: + vg_idx = vg.index + dumped_vg = {} + dumped_vg['name'] = vg.name - vertices = [] + vertices = [] - for i, v in enumerate(getattr(instance.data, points_attr)): - for vg in v.groups: - if vg.group == vg_idx: - vertices.append({ - 'index': i, - 'weight': vg.weight - }) + for i, v in enumerate(getattr(instance.data, points_attr)): + for vg in v.groups: + if vg.group == vg_idx: + vertices.append({ + 'index': i, + 'weight': vg.weight + }) - dumped_vg['vertices'] = vertices + dumped_vg['vertices'] = vertices - vg_data.append(dumped_vg) + vg_data.append(dumped_vg) - data['vertex_groups'] = vg_data + data['vertex_groups'] = vg_data # SHAPE KEYS object_data = instance.data From c5e20085f01fa660de2f5c175b7f1bb60eb2eafe Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 15 Jan 2021 17:02:29 +0100 Subject: [PATCH 19/37] clean: move flush history to the debug logs --- multi_user/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/utils.py b/multi_user/utils.py index 8667b2d..25444f9 100644 --- a/multi_user/utils.py +++ b/multi_user/utils.py @@ -67,7 +67,7 @@ def get_datablock_users(datablock): def flush_history(): try: - logging.info("Flushing history") + logging.debug("Flushing history") for i in range(bpy.context.preferences.edit.undo_steps+1): bpy.ops.ed.undo_push(message="Multiuser history flush") except RuntimeError: From a4ef8a6344d300e47466618dab3f772fbe8b23d8 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 15 Jan 2021 23:14:45 +0100 Subject: [PATCH 20/37] feat: skip update in sculpt mode by default --- multi_user/bl_types/bl_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_mesh.py b/multi_user/bl_types/bl_mesh.py index 26611e6..861989c 100644 --- a/multi_user/bl_types/bl_mesh.py +++ b/multi_user/bl_types/bl_mesh.py @@ -126,7 +126,7 @@ class BlMesh(BlDatablock): def _dump_implementation(self, data, instance=None): assert(instance) - if instance.is_editmode and not self.preferences.sync_flags.sync_during_editmode: + if (instance.is_editmode or bpy.context.mode == "SCULPT") and not self.preferences.sync_flags.sync_during_editmode: raise ContextError("Mesh is in edit mode") mesh = instance From 7a716b4c3704f50620f7a557ec4dc6bdbd25739f Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 15 Jan 2021 23:43:35 +0100 Subject: [PATCH 21/37] feat: auto remove nodes on undo/redo feat: purge operator --- multi_user/operators.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index 830a23c..9ee82dc 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -702,6 +702,33 @@ class SessionClearCache(bpy.types.Operator): row = self.layout row.label(text=f" Do you really want to remove local cache ? ") +class SessionPurgeOperator(bpy.types.Operator): + "Remove node with lost references" + bl_idname = "session.purge" + bl_label = "Purge session data" + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + cache_dir = utils.get_preferences().cache_directory + try: + sanitize_deps_graph(None) + + except Exception as e: + self.report({'ERROR'}, repr(e)) + + return {"FINISHED"} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + row = self.layout + row.label(text=f" Do you really want to remove local cache ? ") + + class SessionNotifyOperator(bpy.types.Operator): """Dialog only operator""" bl_idname = "session.notify" @@ -918,6 +945,7 @@ classes = ( SessionSaveBackupOperator, SessionLoadSaveOperator, SessionStopAutoSaveOperator, + SessionPurgeOperator, ) @@ -931,9 +959,15 @@ def sanitize_deps_graph(dummy): """ if session and session.state['STATE'] == STATE_ACTIVE: start = utils.current_milli_time() + items_to_remove = [] for node_key in session.list(): node = session.get(node_key) - node.resolve(construct=False) + if not node.resolve(construct=False): + try: + session.remove(node.uuid) + except NonAuthorizedOperationError: + continue + logging.debug(f"Sanitize took { utils.current_milli_time()-start}ms") @@ -966,7 +1000,7 @@ def depsgraph_evaluation(scene): if update.id.uuid: # Retrieve local version node = session.get(uuid=update.id.uuid) - + # Check our right on this update: # - if its ours or ( under common and diff), launch the # update process @@ -980,6 +1014,7 @@ def depsgraph_evaluation(scene): try: if node.has_changed(): + logging.info(len(session.list())) session.commit(node.uuid) session.push(node.uuid, check_data=False) except ReferenceError: From 9cc1c92e0ec84da15c38aa613006b1c73b74ed63 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 20 Jan 2021 16:02:15 +0100 Subject: [PATCH 22/37] clean: remove unused var --- multi_user/operators.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index 9ee82dc..9cf4d38 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -712,10 +712,8 @@ class SessionPurgeOperator(bpy.types.Operator): return True def execute(self, context): - cache_dir = utils.get_preferences().cache_directory try: sanitize_deps_graph(None) - except Exception as e: self.report({'ERROR'}, repr(e)) @@ -959,17 +957,18 @@ def sanitize_deps_graph(dummy): """ if session and session.state['STATE'] == STATE_ACTIVE: start = utils.current_milli_time() - items_to_remove = [] + rm_cpt = 0 for node_key in session.list(): node = session.get(node_key) if not node.resolve(construct=False): try: session.remove(node.uuid) + rm_cpt+=1 except NonAuthorizedOperationError: continue logging.debug(f"Sanitize took { utils.current_milli_time()-start}ms") - + logging.info(f"Removed {rm_cpt} nodes") @persistent @@ -1014,7 +1013,6 @@ def depsgraph_evaluation(scene): try: if node.has_changed(): - logging.info(len(session.list())) session.commit(node.uuid) session.push(node.uuid, check_data=False) except ReferenceError: From 8f95158f083148de900678aa1df790bfdea7d2c6 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 21 Jan 2021 14:48:07 +0100 Subject: [PATCH 23/37] feat: initial support for new scenes --- multi_user/bl_types/bl_scene.py | 6 +----- multi_user/operators.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index df7e9a6..95e72ab 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -281,13 +281,9 @@ class BlScene(BlDatablock): bl_icon = 'SCENE_DATA' bl_reload_parent = False - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.diff_method = DIFF_JSON - def _construct(self, data): instance = bpy.data.scenes.new(data["name"]) + instance.uuid = self.uuid return instance def _load_implementation(self, data, target): diff --git a/multi_user/operators.py b/multi_user/operators.py index 9cf4d38..2f78759 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -1025,8 +1025,15 @@ def depsgraph_evaluation(scene): logging.error(e) else: continue - - + # A new scene is created + elif isinstance(update.id, bpy.types.Scene): + ref = session.get(reference=update.id) + if ref: + ref.resolve() + else: + scn_uuid = session.add(update.id) + session.commit(scn_uuid) + session.push(scn_uuid, check_data=False) def register(): from bpy.utils import register_class From 1cfb4e797ebd4c24e3510910005ab2182ebf0ea2 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 22 Jan 2021 11:18:28 +0100 Subject: [PATCH 24/37] fix: handle none uuid --- multi_user/operators.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index 2f78759..a6db607 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -960,16 +960,13 @@ def sanitize_deps_graph(dummy): rm_cpt = 0 for node_key in session.list(): node = session.get(node_key) - if not node.resolve(construct=False): + if node is None or not node.resolve(construct=False): try: session.remove(node.uuid) rm_cpt+=1 except NonAuthorizedOperationError: continue - - logging.debug(f"Sanitize took { utils.current_milli_time()-start}ms") - logging.info(f"Removed {rm_cpt} nodes") - + logging.debug(f"Sanitize took { utils.current_milli_time()-start}ms, Removed {rm_cpt} nodes") @persistent def load_pre_handler(dummy): From 4d69faf186814768e92710b58cd2adbede949659 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 22 Jan 2021 14:24:19 +0100 Subject: [PATCH 25/37] clan: remove diff_method --- multi_user/__init__.py | 2 +- multi_user/bl_types/bl_datablock.py | 5 ----- multi_user/bl_types/bl_file.py | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 2cfe4a1..afc505c 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.23'), + ("replication", '0.1.24'), } diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index b33fe5d..40b9944 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -129,11 +129,6 @@ class BlDatablock(ReplicatedDatablock): if instance and hasattr(instance, 'uuid'): instance.uuid = self.uuid - if logging.getLogger().level == logging.DEBUG: - self.diff_method = DIFF_JSON - else: - self.diff_method = DIFF_BINARY - def resolve(self, construct = True): datablock_ref = None datablock_root = getattr(bpy.data, self.bl_id) diff --git a/multi_user/bl_types/bl_file.py b/multi_user/bl_types/bl_file.py index c4e7946..f39106e 100644 --- a/multi_user/bl_types/bl_file.py +++ b/multi_user/bl_types/bl_file.py @@ -69,7 +69,6 @@ class BlFile(ReplicatedDatablock): raise FileNotFoundError(str(self.instance)) self.preferences = utils.get_preferences() - self.diff_method = DIFF_BINARY def resolve(self, construct = True): if self.data: From 0325e9d0bd4a595f6cba7a12dd09b938940c0902 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 28 Jan 2021 23:59:19 +0100 Subject: [PATCH 26/37] feat: move sequencer to scene --- multi_user/bl_types/__init__.py | 2 +- multi_user/bl_types/bl_scene.py | 143 +++++++++++++++++-- multi_user/bl_types/bl_sequencer.py | 198 --------------------------- multi_user/bl_types/dump_anything.py | 4 +- 4 files changed, 139 insertions(+), 208 deletions(-) delete mode 100644 multi_user/bl_types/bl_sequencer.py diff --git a/multi_user/bl_types/__init__.py b/multi_user/bl_types/__init__.py index 11e41d5..96a5e38 100644 --- a/multi_user/bl_types/__init__.py +++ b/multi_user/bl_types/__init__.py @@ -39,7 +39,7 @@ __all__ = [ 'bl_font', 'bl_sound', 'bl_file', - 'bl_sequencer', + # 'bl_sequencer', 'bl_node_group', 'bl_texture', ] # Order here defines execution order diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index 95e72ab..2372ef0 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -17,18 +17,20 @@ import logging +from pathlib import Path import bpy import mathutils from deepdiff import DeepDiff from replication.constants import DIFF_JSON, MODIFIED +from ..utils import flush_history, current_milli_time from .bl_collection import (dump_collection_children, dump_collection_objects, load_collection_childrens, load_collection_objects, resolve_collection_dependencies) from .bl_datablock import BlDatablock +from .bl_file import get_filepath from .dump_anything import Dumper, Loader -from ..utils import flush_history RENDER_SETTINGS = [ 'dither_intensity', @@ -266,10 +268,105 @@ VIEW_SETTINGS = [ ] +def dump_sequence(sequence: bpy.types.Sequence) -> dict: + """ Dump a sequence to a dict + + :arg sequence: sequence to dump + :type sequence: bpy.types.Sequence + :return dict: + """ + dumper = Dumper() + dumper.exclude_filter = [ + 'lock', + 'select', + 'select_left_handle', + 'select_right_handle', + 'strobe' + ] + dumper.depth = 1 + data = dumper.dump(sequence) + # TODO: Support multiple images + if sequence.type == 'IMAGE': + data['filenames'] = [e.filename for e in sequence.elements] + # Effect strip inputs + input_count = getattr(sequence, 'input_count', None) + if input_count: + for n in range(input_count): + input_name = f"input_{n+1}" + data[input_name] = getattr(sequence, input_name).name + + return data + + +def load_sequence(sequence_data: dict, sequence_editor: bpy.types.SequenceEditor): + """ Load sequence from dumped data + + :arg sequence_data: sequence to dump + :type sequence_data:dict + :arg sequence_editor: root sequence editor + :type sequence_editor: bpy.types.SequenceEditor + """ + start = current_milli_time() + strip_type = sequence_data.get('type') + strip_name = sequence_data.get('name') + strip_channel = sequence_data.get('channel') + strip_frame_start = sequence_data.get('frame_start') + + sequence = sequence_editor.sequences_all.get(strip_name, None) + + if sequence is None: + if strip_type == 'SCENE': + strip_scene = bpy.data.scenes.get(sequence_data.get('scene')) + sequence = sequence_editor.sequences.new_scene(strip_name, + strip_scene, + strip_channel, + strip_frame_start) + elif strip_type == 'MOVIE': + filepath = get_filepath(Path(sequence_data['filepath']).name) + sequence = sequence_editor.sequences.new_movie(strip_name, + filepath, + strip_channel, + strip_frame_start) + elif strip_type == 'SOUND': + filepath = bpy.data.sounds[sequence_data['sound']].filepath + sequence = sequence_editor.sequences.new_sound(strip_name, + filepath, + strip_channel, + strip_frame_start) + elif strip_type == 'IMAGE': + images_name = sequence_data.get('filenames') + filepath = get_filepath(images_name[0]) + sequence = sequence_editor.sequences.new_image(strip_name, + filepath, + strip_channel, + strip_frame_start) + # load other images + if len(images_name)>1: + for img_idx in range(1,len(images_name)): + sequence.elements.append((images_name[img_idx])) + else: + seq = {} + + for i in range(sequence_data['input_count']): + seq[f"seq{i+1}"] = sequence_editor.sequences_all.get(sequence_data.get(f"input_{i+1}", None)) + + sequence = sequence_editor.sequences.new_effect(name=strip_name, + type=strip_type, + channel=strip_channel, + frame_start=strip_frame_start, + frame_end=sequence_data['frame_final_end'], + **seq) + + loader = Loader() + loader.exclure_filter = ['filepath', 'sound', 'filenames','fps'] + loader.load(sequence, sequence_data) + sequence.select = False + logging.info(f"loading {strip_name} took {current_milli_time()-start} ms") + class BlScene(BlDatablock): bl_id = "scenes" @@ -284,6 +381,7 @@ class BlScene(BlDatablock): def _construct(self, data): instance = bpy.data.scenes.new(data["name"]) instance.uuid = self.uuid + return instance def _load_implementation(self, data, target): @@ -325,6 +423,22 @@ class BlScene(BlDatablock): 'view_settings']['curve_mapping']['black_level'] target.view_settings.curve_mapping.update() + # Sequencer + sequences = data.get('sequences') + + if sequences: + # Create sequencer data + target.sequence_editor_create() + vse = target.sequence_editor + for seq in vse.sequences_all: + if seq.name not in sequences: + vse.sequences.remove(seq) + + for seq_name, seq_data in sequences.items(): + load_sequence(seq_data, vse) + elif target.sequence_editor and not sequences: + target.sequence_editor_clear() + # FIXME: Find a better way after the replication big refacotoring # Keep other user from deleting collection object by flushing their history flush_history() @@ -387,10 +501,14 @@ class BlScene(BlDatablock): data['view_settings']['curve_mapping']['curves'] = scene_dumper.dump( instance.view_settings.curve_mapping.curves) - if instance.sequence_editor: - data['has_sequence'] = True - else: - data['has_sequence'] = False + # Sequence + vse = instance.sequence_editor + if vse: + dumped_sequences = {} + for seq in vse.sequences_all: + dumped_sequences[seq.name] = dump_sequence(seq) + data['sequences'] = dumped_sequences + return data @@ -409,9 +527,18 @@ class BlScene(BlDatablock): deps.append(self.instance.grease_pencil) # Sequences - # deps.extend(list(self.instance.sequence_editor.sequences_all)) - if self.instance.sequence_editor: - deps.append(self.instance.sequence_editor) + vse = self.instance.sequence_editor + if vse: + for sequence in vse.sequences_all: + if sequence.type == 'MOVIE' and sequence.filepath: + deps.append(Path(bpy.path.abspath(sequence.filepath))) + elif sequence.type == 'SOUND' and sequence.sound: + deps.append(sequence.sound) + elif sequence.type == 'IMAGE': + for elem in sequence.elements: + sequence.append( + Path(bpy.path.abspath(sequence.directory), + elem.filename)) return deps diff --git a/multi_user/bl_types/bl_sequencer.py b/multi_user/bl_types/bl_sequencer.py deleted file mode 100644 index d961ddc..0000000 --- a/multi_user/bl_types/bl_sequencer.py +++ /dev/null @@ -1,198 +0,0 @@ -# ##### 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 bpy -import mathutils -from pathlib import Path -import logging - -from .bl_file import get_filepath -from .dump_anything import Loader, Dumper -from .bl_datablock import BlDatablock, get_datablock_from_uuid - -def dump_sequence(sequence: bpy.types.Sequence) -> dict: - """ Dump a sequence to a dict - - :arg sequence: sequence to dump - :type sequence: bpy.types.Sequence - :return dict: - """ - dumper = Dumper() - dumper.exclude_filter = [ - 'lock', - 'select', - 'select_left_handle', - 'select_right_handle', - 'strobe' - ] - dumper.depth = 1 - data = dumper.dump(sequence) - - - # TODO: Support multiple images - if sequence.type == 'IMAGE': - data['filenames'] = [e.filename for e in sequence.elements] - - - # Effect strip inputs - input_count = getattr(sequence, 'input_count', None) - if input_count: - for n in range(input_count): - input_name = f"input_{n+1}" - data[input_name] = getattr(sequence, input_name).name - - return data - - -def load_sequence(sequence_data: dict, sequence_editor: bpy.types.SequenceEditor): - """ Load sequence from dumped data - - :arg sequence_data: sequence to dump - :type sequence_data:dict - :arg sequence_editor: root sequence editor - :type sequence_editor: bpy.types.SequenceEditor - """ - strip_type = sequence_data.get('type') - strip_name = sequence_data.get('name') - strip_channel = sequence_data.get('channel') - strip_frame_start = sequence_data.get('frame_start') - - sequence = sequence_editor.sequences_all.get(strip_name, None) - - if sequence is None: - if strip_type == 'SCENE': - strip_scene = bpy.data.scenes.get(sequence_data.get('scene')) - sequence = sequence_editor.sequences.new_scene(strip_name, - strip_scene, - strip_channel, - strip_frame_start) - elif strip_type == 'MOVIE': - filepath = get_filepath(Path(sequence_data['filepath']).name) - sequence = sequence_editor.sequences.new_movie(strip_name, - filepath, - strip_channel, - strip_frame_start) - elif strip_type == 'SOUND': - filepath = bpy.data.sounds[sequence_data['sound']].filepath - sequence = sequence_editor.sequences.new_sound(strip_name, - filepath, - strip_channel, - strip_frame_start) - elif strip_type == 'IMAGE': - images_name = sequence_data.get('filenames') - filepath = get_filepath(images_name[0]) - sequence = sequence_editor.sequences.new_image(strip_name, - filepath, - strip_channel, - strip_frame_start) - # load other images - if len(images_name)>1: - for img_idx in range(1,len(images_name)): - sequence.elements.append((images_name[img_idx])) - else: - seq = {} - - for i in range(sequence_data['input_count']): - seq[f"seq{i+1}"] = sequence_editor.sequences_all.get(sequence_data.get(f"input_{i+1}", None)) - - sequence = sequence_editor.sequences.new_effect(name=strip_name, - type=strip_type, - channel=strip_channel, - frame_start=strip_frame_start, - frame_end=sequence_data['frame_final_end'], - **seq) - - loader = Loader() - loader.load(sequence, sequence_data) - sequence.select = False - - -class BlSequencer(BlDatablock): - bl_id = "scenes" - bl_class = bpy.types.SequenceEditor - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True - bl_check_common = True - bl_icon = 'SEQUENCE' - bl_reload_parent = False - - def _construct(self, data): - # Get the scene - scene_id = data.get('name') - scene = bpy.data.scenes.get(scene_id, None) - - # Create sequencer data - scene.sequence_editor_clear() - scene.sequence_editor_create() - - return scene.sequence_editor - - def resolve(self, construct = True): - scene = bpy.data.scenes.get(self.data['name'], None) - if scene: - if scene.sequence_editor is None and construct: - self.instance = self._construct(self.data) - else: - self.instance = scene.sequence_editor - else: - logging.warning("Sequencer editor scene not found") - - def _load_implementation(self, data, target): - loader = Loader() - # Sequencer - sequences = data.get('sequences') - if sequences: - for seq in target.sequences_all: - if seq.name not in sequences: - target.sequences.remove(seq) - for seq_name, seq_data in sequences.items(): - load_sequence(seq_data, target) - - def _dump_implementation(self, data, instance=None): - assert(instance) - sequence_dumper = Dumper() - sequence_dumper.depth = 1 - sequence_dumper.include_filter = [ - 'proxy_storage', - ] - data = {}#sequence_dumper.dump(instance) - # Sequencer - sequences = {} - - for seq in instance.sequences_all: - sequences[seq.name] = dump_sequence(seq) - - data['sequences'] = sequences - data['name'] = instance.id_data.name - - return data - - - def _resolve_deps_implementation(self): - deps = [] - - for seq in self.instance.sequences_all: - if seq.type == 'MOVIE' and seq.filepath: - deps.append(Path(bpy.path.abspath(seq.filepath))) - elif seq.type == 'SOUND' and seq.sound: - deps.append(seq.sound) - elif seq.type == 'IMAGE': - for e in seq.elements: - deps.append(Path(bpy.path.abspath(seq.directory), e.filename)) - return deps diff --git a/multi_user/bl_types/dump_anything.py b/multi_user/bl_types/dump_anything.py index 4d43180..30c2a13 100644 --- a/multi_user/bl_types/dump_anything.py +++ b/multi_user/bl_types/dump_anything.py @@ -465,6 +465,7 @@ class Loader: self.type_subset = self.match_subset_all self.occlude_read_only = False self.order = ['*'] + self.exclure_filter = [] def load(self, dst_data, src_dumped_data): self._load_any( @@ -475,7 +476,8 @@ class Loader: def _load_any(self, any, dump): for filter_function, load_function in self.type_subset: - if filter_function(any): + if filter_function(any) and \ + any.sub_element_name not in self.exclure_filter: load_function(any, dump) return From ee4083c134545dcedd03a3430a42352b11818706 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 29 Jan 2021 11:28:50 +0100 Subject: [PATCH 27/37] feat: evaluate gpencil on layer change/ frame change/ mode change Related to #166 --- multi_user/bl_types/bl_gpencil.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_gpencil.py b/multi_user/bl_types/bl_gpencil.py index be6b649..90e9eac 100644 --- a/multi_user/bl_types/bl_gpencil.py +++ b/multi_user/bl_types/bl_gpencil.py @@ -204,7 +204,7 @@ def dump_layer(layer): for frame in layer.frames: dumped_layer['frames'].append(dump_frame(frame)) - + return dumped_layer @@ -287,6 +287,8 @@ class BlGpencil(BlDatablock): for layer in instance.layers: data['layers'][layer.info] = dump_layer(layer) + data["active_layers"] = instance.layers.active.info + data["eval_frame"] = bpy.context.scene.frame_current return data def _resolve_deps_implementation(self): @@ -296,3 +298,17 @@ class BlGpencil(BlDatablock): deps.append(material) return deps + + def layer_changed(self): + return self.instance.layers.active.info != self.data["active_layers"] + + def frame_changed(self): + return bpy.context.scene.frame_current != self.data["eval_frame"] + + def diff(self): + if self.layer_changed() \ + or self.frame_changed() \ + or bpy.context.mode == 'OBJECT': + return super().diff() + else: + return False From c855b5a42494eaa3ce53551b66e7470372fa0fd0 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 29 Jan 2021 11:39:00 +0100 Subject: [PATCH 28/37] clean: removed logs --- multi_user/bl_types/bl_scene.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index 2372ef0..3ad8774 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -24,7 +24,7 @@ import mathutils from deepdiff import DeepDiff from replication.constants import DIFF_JSON, MODIFIED -from ..utils import flush_history, current_milli_time +from ..utils import flush_history from .bl_collection import (dump_collection_children, dump_collection_objects, load_collection_childrens, load_collection_objects, resolve_collection_dependencies) @@ -310,7 +310,6 @@ def load_sequence(sequence_data: dict, sequence_editor: bpy.types.SequenceEditor :arg sequence_editor: root sequence editor :type sequence_editor: bpy.types.SequenceEditor """ - start = current_milli_time() strip_type = sequence_data.get('type') strip_name = sequence_data.get('name') strip_channel = sequence_data.get('channel') @@ -362,10 +361,10 @@ def load_sequence(sequence_data: dict, sequence_editor: bpy.types.SequenceEditor **seq) loader = Loader() + # TODO: Support filepath updates loader.exclure_filter = ['filepath', 'sound', 'filenames','fps'] loader.load(sequence, sequence_data) sequence.select = False - logging.info(f"loading {strip_name} took {current_milli_time()-start} ms") class BlScene(BlDatablock): @@ -430,12 +429,15 @@ class BlScene(BlDatablock): # Create sequencer data target.sequence_editor_create() vse = target.sequence_editor + + # Clear removed sequences for seq in vse.sequences_all: if seq.name not in sequences: vse.sequences.remove(seq) - + # Load existing sequences for seq_name, seq_data in sequences.items(): load_sequence(seq_data, vse) + # If the sequence is no longer used, clear it elif target.sequence_editor and not sequences: target.sequence_editor_clear() From 589702dab723edb45019fb388898c18ccd4c1593 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 29 Jan 2021 11:54:13 +0100 Subject: [PATCH 29/37] feat: continous update support by toggling the sync_in_editmode flag Related to #166 --- multi_user/bl_types/bl_gpencil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_gpencil.py b/multi_user/bl_types/bl_gpencil.py index 90e9eac..91b69c2 100644 --- a/multi_user/bl_types/bl_gpencil.py +++ b/multi_user/bl_types/bl_gpencil.py @@ -308,7 +308,8 @@ class BlGpencil(BlDatablock): def diff(self): if self.layer_changed() \ or self.frame_changed() \ - or bpy.context.mode == 'OBJECT': + or bpy.context.mode == 'OBJECT' \ + or self.preferences.sync_flags.sync_during_editmode: return super().diff() else: return False From 86cb3d29fbd1b57312ff7863e0b4ba0af7fe4ea9 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 29 Jan 2021 16:29:27 +0100 Subject: [PATCH 30/37] fix: wrong grease pencil frame after layer update --- multi_user/bl_types/bl_gpencil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_gpencil.py b/multi_user/bl_types/bl_gpencil.py index 91b69c2..11d5ef8 100644 --- a/multi_user/bl_types/bl_gpencil.py +++ b/multi_user/bl_types/bl_gpencil.py @@ -226,7 +226,6 @@ def load_layer(layer_data, layer): load_frame(frame_data, target_frame) - class BlGpencil(BlDatablock): bl_id = "grease_pencils" bl_class = bpy.types.GreasePencil @@ -265,6 +264,7 @@ class BlGpencil(BlDatablock): load_layer(layer_data, target_layer) + target.layers.update() From cf0d7a11220b457d56eac2aa576115aad97849a7 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 5 Feb 2021 10:25:50 +0100 Subject: [PATCH 31/37] fix: temporary for stroke geometry update to fix triangulation with fill materials Related to 170 --- multi_user/bl_types/bl_gpencil.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_gpencil.py b/multi_user/bl_types/bl_gpencil.py index 11d5ef8..bcc1296 100644 --- a/multi_user/bl_types/bl_gpencil.py +++ b/multi_user/bl_types/bl_gpencil.py @@ -109,7 +109,9 @@ def load_stroke(stroke_data, stroke): stroke.points.add(stroke_data["p_count"]) np_load_collection(stroke_data['points'], stroke.points, STROKE_POINT) - + # HACK: Temporary fix to trigger a BKE_gpencil_stroke_geometry_update to + # fix fill issues + stroke.uv_scale = stroke_data["uv_scale"] def dump_frame(frame): """ Dump a grease pencil frame to a dict From 8262fb9d4eb78f1a111696a05f212a1c6632f188 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 5 Feb 2021 11:50:58 +0100 Subject: [PATCH 32/37] feat: time stamped logs files --- multi_user/operators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index a6db607..c3ff65b 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -165,9 +165,10 @@ class SessionStartOperator(bpy.types.Operator): datefmt='%H:%M:%S' ) + start_time = datetime.now().strftime('%Y_%m_%d_%H-%M-%S') log_directory = os.path.join( settings.cache_directory, - "multiuser_client.log") + f"multiuser_{start_time}.log") os.makedirs(settings.cache_directory, exist_ok=True) From 1f0f44fdbf89db19fba3de6416f3497f0e30fb02 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 9 Feb 2021 14:14:53 +0100 Subject: [PATCH 33/37] clean: materials dump --- multi_user/bl_types/bl_curve.py | 19 +++------ multi_user/bl_types/bl_material.py | 68 ++++++++++++++++++++++++------ multi_user/bl_types/bl_mesh.py | 22 +++------- multi_user/bl_types/bl_volume.py | 20 +++------ 4 files changed, 71 insertions(+), 58 deletions(-) diff --git a/multi_user/bl_types/bl_curve.py b/multi_user/bl_types/bl_curve.py index 7121b3a..029e495 100644 --- a/multi_user/bl_types/bl_curve.py +++ b/multi_user/bl_types/bl_curve.py @@ -27,6 +27,7 @@ from .dump_anything import (Dumper, Loader, np_load_collection, np_dump_collection) from .bl_datablock import get_datablock_from_uuid +from .bl_material import dump_materials_slots, load_materials_slots SPLINE_BEZIER_POINT = [ # "handle_left_type", @@ -173,18 +174,9 @@ class BlCurve(BlDatablock): loader.load(new_spline, spline) # MATERIAL SLOTS - target.materials.clear() - for mat_uuid, mat_name in data["material_list"]: - mat_ref = None - if mat_uuid is not None: - mat_ref = get_datablock_from_uuid(mat_uuid, None) - else: - mat_ref = bpy.data.materials.get(mat_name, None) - - if mat_ref is None: - raise Exception("Material doesn't exist") - - target.materials.append(mat_ref) + src_materials = data.get('materials', None) + if src_materials: + load_materials_slots(src_materials, target.materials) def _dump_implementation(self, data, instance=None): assert(instance) @@ -229,8 +221,7 @@ class BlCurve(BlDatablock): elif isinstance(instance, T.Curve): data['type'] = 'CURVE' - data['material_list'] = [(m.uuid, m.name) - for m in instance.materials if m] + data['materials'] = dump_materials_slots(instance.materials) return data diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 1080912..c2a42b5 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -29,7 +29,7 @@ from .bl_datablock import BlDatablock, get_datablock_from_uuid NODE_SOCKET_INDEX = re.compile('\[(\d*)\]') -def load_node(node_data, node_tree): +def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): """ Load a node into a node_tree from a dict :arg node_data: dumped node data @@ -70,9 +70,11 @@ def load_node(node_data, node_tree): try: outputs[idx].default_value = output except: - logging.warning(f"Node {target_node.name} output {outputs[idx].name} parameter not supported, skipping ({e})") + logging.warning( + f"Node {target_node.name} output {outputs[idx].name} parameter not supported, skipping ({e})") else: - logging.warning(f"Node {target_node.name} output length mismatch.") + logging.warning( + f"Node {target_node.name} output length mismatch.") def load_links(links_data, node_tree): @@ -117,7 +119,7 @@ def dump_links(links): return links_data -def dump_node(node): +def dump_node(node: bpy.types.ShaderNode) -> dict: """ Dump a single node to a dict :arg node: target node @@ -155,7 +157,7 @@ def dump_node(node): dumped_node = node_dumper.dump(node) - dump_io_needed = (node.type not in ['REROUTE','OUTPUT_MATERIAL']) + dump_io_needed = (node.type not in ['REROUTE', 'OUTPUT_MATERIAL']) if dump_io_needed: io_dumper = Dumper() @@ -166,13 +168,15 @@ def dump_node(node): dumped_node['inputs'] = [] for idx, inpt in enumerate(node.inputs): if hasattr(inpt, 'default_value'): - dumped_node['inputs'].append(io_dumper.dump(inpt.default_value)) + dumped_node['inputs'].append( + io_dumper.dump(inpt.default_value)) if hasattr(node, 'outputs'): dumped_node['outputs'] = [] for idx, output in enumerate(node.outputs): if hasattr(output, 'default_value'): - dumped_node['outputs'].append(io_dumper.dump(output.default_value)) + dumped_node['outputs'].append( + io_dumper.dump(output.default_value)) if hasattr(node, 'color_ramp'): ramp_dumper = Dumper() @@ -223,7 +227,7 @@ def dump_shader_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict: return node_tree_data -def dump_node_tree_sockets(sockets: bpy.types.Collection)->dict: +def dump_node_tree_sockets(sockets: bpy.types.Collection) -> dict: """ dump sockets of a shader_node_tree :arg target_node_tree: target node_tree @@ -244,6 +248,7 @@ def dump_node_tree_sockets(sockets: bpy.types.Collection)->dict: return sockets_data + def load_node_tree_sockets(sockets: bpy.types.Collection, sockets_data: dict): """ load sockets of a shader_node_tree @@ -263,7 +268,7 @@ def load_node_tree_sockets(sockets: bpy.types.Collection, # Check for new sockets for idx, socket_data in enumerate(sockets_data): try: - checked_socket = sockets[idx] + checked_socket = sockets[idx] if checked_socket.name != socket_data[0]: checked_socket.name = socket_data[0] except Exception: @@ -271,7 +276,7 @@ def load_node_tree_sockets(sockets: bpy.types.Collection, s['uuid'] = socket_data[2] -def load_shader_node_tree(node_tree_data:dict, target_node_tree:bpy.types.ShaderNodeTree)->dict: +def load_shader_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeTree) -> dict: """Load a shader node_tree from dumped data :arg node_tree_data: dumped node data @@ -291,7 +296,7 @@ def load_shader_node_tree(node_tree_data:dict, target_node_tree:bpy.types.Shader if 'outputs' in node_tree_data: socket_collection = getattr(target_node_tree, 'outputs') - load_node_tree_sockets(socket_collection,node_tree_data['outputs']) + load_node_tree_sockets(socket_collection, node_tree_data['outputs']) # Load nodes for node in node_tree_data["nodes"]: @@ -305,8 +310,11 @@ def load_shader_node_tree(node_tree_data:dict, target_node_tree:bpy.types.Shader def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list: - has_image = lambda node : (node.type in ['TEX_IMAGE', 'TEX_ENVIRONMENT'] and node.image) - has_node_group = lambda node : (hasattr(node,'node_tree') and node.node_tree) + def has_image(node): return ( + node.type in ['TEX_IMAGE', 'TEX_ENVIRONMENT'] and node.image) + + def has_node_group(node): return ( + hasattr(node, 'node_tree') and node.node_tree) deps = [] @@ -319,6 +327,40 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list: return deps +def dump_materials_slots(materials: bpy.types.bpy_prop_collection) -> list: + """ Dump material slots collection + + :arg materials: material slots collection to dump + :type materials: bpy.types.bpy_prop_collection + :return: list of tuples (mat_uuid, mat_name) + """ + return [(m.uuid, m.name) for m in materials if m] + + +def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_collection): + """ Load material slots + + :arg src_materials: dumped material collection (ex: object.materials) + :type src_materials: list of tuples (uuid, name) + :arg dst_materials: target material collection pointer + :type dst_materials: bpy.types.bpy_prop_collection + """ + # MATERIAL SLOTS + dst_materials.clear() + + for mat_uuid, mat_name in src_materials: + mat_ref = None + if mat_uuid is not None: + mat_ref = get_datablock_from_uuid(mat_uuid, None) + else: + mat_ref = bpy.data.materials.get(mat_name, None) + + if mat_ref is None: + raise Exception(f"Material {mat_name} doesn't exist") + + dst_materials.append(mat_ref) + + class BlMaterial(BlDatablock): bl_id = "materials" bl_class = bpy.types.Material diff --git a/multi_user/bl_types/bl_mesh.py b/multi_user/bl_types/bl_mesh.py index 861989c..8e6afbb 100644 --- a/multi_user/bl_types/bl_mesh.py +++ b/multi_user/bl_types/bl_mesh.py @@ -26,6 +26,7 @@ from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dum from replication.constants import DIFF_BINARY from replication.exception import ContextError from .bl_datablock import BlDatablock, get_datablock_from_uuid +from .bl_material import dump_materials_slots, load_materials_slots VERTICE = ['co'] @@ -69,19 +70,9 @@ class BlMesh(BlDatablock): loader.load(target, data) # MATERIAL SLOTS - target.materials.clear() - - for mat_uuid, mat_name in data["material_list"]: - mat_ref = None - if mat_uuid is not None: - mat_ref = get_datablock_from_uuid(mat_uuid, None) - else: - mat_ref = bpy.data.materials.get(mat_name, None) - - if mat_ref is None: - raise Exception("Material doesn't exist") - - target.materials.append(mat_ref) + src_materials = data.get('materials', None) + if src_materials: + load_materials_slots(src_materials, target.materials) # CLEAR GEOMETRY if target.vertices: @@ -172,9 +163,8 @@ class BlMesh(BlDatablock): data['vertex_colors'][color_map.name] = {} data['vertex_colors'][color_map.name]['data'] = np_dump_collection_primitive(color_map.data, 'color') - # Fix material index - data['material_list'] = [(m.uuid, m.name) for m in instance.materials if m] - + # Materials + data['materials'] = dump_materials_slots(instance.materials) return data def _resolve_deps_implementation(self): diff --git a/multi_user/bl_types/bl_volume.py b/multi_user/bl_types/bl_volume.py index fc67012..93b490c 100644 --- a/multi_user/bl_types/bl_volume.py +++ b/multi_user/bl_types/bl_volume.py @@ -22,7 +22,7 @@ from pathlib import Path from .dump_anything import Loader, Dumper from .bl_datablock import BlDatablock, get_datablock_from_uuid - +from .bl_material import dump_materials_slots, load_materials_slots class BlVolume(BlDatablock): bl_id = "volumes" @@ -40,19 +40,9 @@ class BlVolume(BlDatablock): loader.load(target.display, data['display']) # MATERIAL SLOTS - target.materials.clear() - - for mat_uuid, mat_name in data["material_list"]: - mat_ref = None - if mat_uuid is not None: - mat_ref = get_datablock_from_uuid(mat_uuid, None) - else: - mat_ref = bpy.data.materials.get(mat_name, None) - - if mat_ref is None: - raise Exception("Material doesn't exist") - - target.materials.append(mat_ref) + src_materials = data.get('materials', None) + if src_materials: + load_materials_slots(src_materials, target.materials) def _construct(self, data): return bpy.data.volumes.new(data["name"]) @@ -78,7 +68,7 @@ class BlVolume(BlDatablock): data['display'] = dumper.dump(instance.display) # Fix material index - data['material_list'] = [(m.uuid, m.name) for m in instance.materials if m] + data['materials'] = dump_materials_slots(instance.materials) return data From c00b2a2d7d9d8bb0cb5ebebd9ba4f76e7da6efe6 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 9 Feb 2021 14:20:08 +0100 Subject: [PATCH 34/37] feat: explicit state loading error --- multi_user/bl_types/bl_object.py | 4 ++++ multi_user/operators.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 2d8304e..281cd08 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -135,6 +135,10 @@ class BlObject(BlDatablock): data_uuid, find_data_from_name(data_id), ignore=['images']) # TODO: use resolve_from_id + + if object_data is None and data_uuid: + raise Exception(f"Fail to load object {data['name']}({self.uuid})") + instance = bpy.data.objects.new(object_name, object_data) instance.uuid = self.uuid diff --git a/multi_user/operators.py b/multi_user/operators.py index c3ff65b..aa5d171 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -78,19 +78,23 @@ def initialize_session(): settings = utils.get_preferences() runtime_settings = bpy.context.window_manager.session - logging.info("Constructing nodes") # Step 1: Constrect nodes + logging.info("Constructing nodes") for node in session._graph.list_ordered(): node_ref = session.get(uuid=node) - if node_ref.state == FETCHED: + if node_ref and node_ref.state == FETCHED: node_ref.resolve() - - logging.info("Loading nodes") + else: + logging.error(f"Can't construct node {node}") + # Step 2: Load nodes + logging.info("Loading nodes") for node in session._graph.list_ordered(): node_ref = session.get(uuid=node) - if node_ref.state == FETCHED: + if node_ref and node_ref.state == FETCHED: node_ref.apply() + else: + logging.error(f"Can't load node {node}") logging.info("Registering timers") # Step 4: Register blender timers From 0b8863125088373ab0d243b09b6f35bca8e259a0 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 9 Feb 2021 18:09:39 +0100 Subject: [PATCH 35/37] refactor: move session dump_db to replication as session.save --- multi_user/__init__.py | 2 +- multi_user/operators.py | 90 ++++++++++++++++------------------------- multi_user/timers.py | 2 +- 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index afc505c..4a56a6e 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.24'), + ("replication", '0.1.25'), } diff --git a/multi_user/operators.py b/multi_user/operators.py index aa5d171..22c63e7 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -82,19 +82,20 @@ def initialize_session(): logging.info("Constructing nodes") for node in session._graph.list_ordered(): node_ref = session.get(uuid=node) - if node_ref and node_ref.state == FETCHED: - node_ref.resolve() - else: + if node_ref is None: logging.error(f"Can't construct node {node}") + elif node_ref.state == FETCHED: + node_ref.resolve() # Step 2: Load nodes logging.info("Loading nodes") for node in session._graph.list_ordered(): node_ref = session.get(uuid=node) - if node_ref and node_ref.state == FETCHED: - node_ref.apply() - else: + + if node_ref is None: logging.error(f"Can't load node {node}") + elif node_ref.state == FETCHED: + node_ref.apply() logging.info("Registering timers") # Step 4: Register blender timers @@ -718,7 +719,7 @@ class SessionPurgeOperator(bpy.types.Operator): def execute(self, context): try: - sanitize_deps_graph(None) + sanitize_deps_graph(remove_nodes=True) except Exception as e: self.report({'ERROR'}, repr(e)) @@ -756,37 +757,6 @@ class SessionNotifyOperator(bpy.types.Operator): return context.window_manager.invoke_props_dialog(self) -def dump_db(filepath): - # Replication graph - nodes_ids = session.list() - #TODO: add dump graph to replication - - nodes =[] - for n in nodes_ids: - nd = session.get(uuid=n) - nodes.append(( - n, - { - 'owner': nd.owner, - 'str_type': nd.str_type, - 'data': nd.data, - 'dependencies': nd.dependencies, - } - )) - - db = dict() - db['nodes'] = nodes - db['users'] = copy.copy(session.online_users) - - stime = datetime.now().strftime('%Y_%m_%d_%H-%M-%S') - - filepath = Path(filepath) - filepath = filepath.with_name(f"{filepath.stem}_{stime}{filepath.suffix}") - with gzip.open(filepath, "wb") as f: - logging.info(f"Writing session snapshot to {filepath}") - pickle.dump(db, f, protocol=4) - - class SessionSaveBackupOperator(bpy.types.Operator, ExportHelper): bl_idname = "session.save" bl_label = "Save session data" @@ -820,7 +790,7 @@ class SessionSaveBackupOperator(bpy.types.Operator, ExportHelper): recorder.register() deleyables.append(recorder) else: - dump_db(self.filepath) + session.save(self.filepath) return {'FINISHED'} @@ -952,26 +922,34 @@ classes = ( ) +def sanitize_deps_graph(remove_nodes: bool = False): + if session and session.state['STATE'] == STATE_ACTIVE: + start = utils.current_milli_time() + rm_cpt = 0 + for node_key in session.list(): + node = session.get(node_key) + if node is None \ + or (node.state == UP and not node.resolve(construct=False)): + if remove_nodes: + try: + session.remove(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") + + @persistent -def sanitize_deps_graph(dummy): - """sanitize deps graph +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'] == STATE_ACTIVE: - start = utils.current_milli_time() - rm_cpt = 0 - for node_key in session.list(): - node = session.get(node_key) - if node is None or not node.resolve(construct=False): - try: - session.remove(node.uuid) - rm_cpt+=1 - except NonAuthorizedOperationError: - continue - logging.debug(f"Sanitize took { utils.current_milli_time()-start}ms, Removed {rm_cpt} nodes") + sanitize_deps_graph(remove_nodes=True) @persistent def load_pre_handler(dummy): @@ -1043,8 +1021,8 @@ def register(): register_class(cls) - bpy.app.handlers.undo_post.append(sanitize_deps_graph) - bpy.app.handlers.redo_post.append(sanitize_deps_graph) + 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) @@ -1058,8 +1036,8 @@ def unregister(): for cls in reversed(classes): unregister_class(cls) - bpy.app.handlers.undo_post.remove(sanitize_deps_graph) - bpy.app.handlers.redo_post.remove(sanitize_deps_graph) + 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/timers.py b/multi_user/timers.py index 62fdeee..60725f3 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -98,7 +98,7 @@ class SessionBackupTimer(Timer): def execute(self): - operators.dump_db(self._filepath) + session.save(self._filepath) class ApplyTimer(Timer): def __init__(self, timeout=1, target_type=None): From cdcb2de786821048b7f7cc31ad530da18049407f Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 12 Feb 2021 10:48:29 +0100 Subject: [PATCH 36/37] clean: remove apply related settings fix: image reloading after modifications --- multi_user/operators.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index 22c63e7..a8f024b 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -208,15 +208,9 @@ class SessionStartOperator(bpy.types.Operator): bpy_factory.register_type( type_module_class.bl_class, type_module_class, - timer=type_local_config.bl_delay_refresh*1000, - automatic=type_local_config.auto_push, check_common=type_module_class.bl_check_common) - if type_local_config.bl_delay_apply > 0: - deleyables.append( - timers.ApplyTimer( - timeout=type_local_config.bl_delay_apply, - target_type=type_module_class)) + deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate)) if bpy.app.version[1] >= 91: python_binary_path = sys.executable @@ -921,8 +915,18 @@ classes = ( SessionPurgeOperator, ) +def update_external_dependencies(): + nodes_ids = session.list(filter=bl_types.bl_file.BlFile) + for node_id in nodes_ids: + node = session.get(node_id) + if node and node.owner in [session.id, RP_COMMON] \ + and node.has_changed(): + session.commit(node_id) + session.push(node_id, check_data=False) def sanitize_deps_graph(remove_nodes: bool = False): + """ Cleanup the replication graph + """ if session and session.state['STATE'] == STATE_ACTIVE: start = utils.current_milli_time() rm_cpt = 0 @@ -973,6 +977,8 @@ def depsgraph_evaluation(scene): dependency_updates = [u for u in blender_depsgraph.updates] settings = utils.get_preferences() + update_external_dependencies() + # NOTE: maybe we don't need to check each update but only the first for update in reversed(dependency_updates): # Is the object tracked ? From 1dd023506102dcd33eba2febfa5d6344fdaced81 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 12 Feb 2021 10:49:04 +0100 Subject: [PATCH 37/37] clean: timer related settings --- multi_user/bl_types/bl_action.py | 3 --- multi_user/bl_types/bl_armature.py | 3 --- multi_user/bl_types/bl_camera.py | 3 --- multi_user/bl_types/bl_collection.py | 3 --- multi_user/bl_types/bl_curve.py | 3 --- multi_user/bl_types/bl_datablock.py | 3 --- multi_user/bl_types/bl_file.py | 3 --- multi_user/bl_types/bl_font.py | 3 --- multi_user/bl_types/bl_gpencil.py | 3 --- multi_user/bl_types/bl_image.py | 3 --- multi_user/bl_types/bl_lattice.py | 3 --- multi_user/bl_types/bl_library.py | 3 --- multi_user/bl_types/bl_light.py | 3 --- multi_user/bl_types/bl_lightprobe.py | 3 --- multi_user/bl_types/bl_material.py | 3 --- multi_user/bl_types/bl_mesh.py | 3 --- multi_user/bl_types/bl_metaball.py | 3 --- multi_user/bl_types/bl_node_group.py | 3 --- multi_user/bl_types/bl_object.py | 3 --- multi_user/bl_types/bl_scene.py | 3 --- multi_user/bl_types/bl_sound.py | 3 --- multi_user/bl_types/bl_speaker.py | 3 --- multi_user/bl_types/bl_texture.py | 3 --- multi_user/bl_types/bl_volume.py | 3 --- multi_user/bl_types/bl_world.py | 3 --- multi_user/preferences.py | 28 ---------------------------- multi_user/timers.py | 21 +++++---------------- multi_user/ui.py | 2 +- 28 files changed, 6 insertions(+), 120 deletions(-) diff --git a/multi_user/bl_types/bl_action.py b/multi_user/bl_types/bl_action.py index 0bee448..8672fb6 100644 --- a/multi_user/bl_types/bl_action.py +++ b/multi_user/bl_types/bl_action.py @@ -132,9 +132,6 @@ def load_fcurve(fcurve_data, fcurve): class BlAction(BlDatablock): bl_id = "actions" bl_class = bpy.types.Action - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'ACTION_TWEAK' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_armature.py b/multi_user/bl_types/bl_armature.py index f106c5b..f39776f 100644 --- a/multi_user/bl_types/bl_armature.py +++ b/multi_user/bl_types/bl_armature.py @@ -38,9 +38,6 @@ def get_roll(bone: bpy.types.Bone) -> float: class BlArmature(BlDatablock): bl_id = "armatures" bl_class = bpy.types.Armature - bl_delay_refresh = 1 - bl_delay_apply = 0 - bl_automatic_push = True bl_check_common = False bl_icon = 'ARMATURE_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_camera.py b/multi_user/bl_types/bl_camera.py index 78b9968..486726c 100644 --- a/multi_user/bl_types/bl_camera.py +++ b/multi_user/bl_types/bl_camera.py @@ -26,9 +26,6 @@ from .bl_datablock import BlDatablock class BlCamera(BlDatablock): bl_id = "cameras" bl_class = bpy.types.Camera - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'CAMERA_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_collection.py b/multi_user/bl_types/bl_collection.py index c9874e4..6393847 100644 --- a/multi_user/bl_types/bl_collection.py +++ b/multi_user/bl_types/bl_collection.py @@ -85,9 +85,6 @@ class BlCollection(BlDatablock): bl_id = "collections" bl_icon = 'FILE_FOLDER' bl_class = bpy.types.Collection - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = True bl_reload_parent = False diff --git a/multi_user/bl_types/bl_curve.py b/multi_user/bl_types/bl_curve.py index 029e495..ec7954e 100644 --- a/multi_user/bl_types/bl_curve.py +++ b/multi_user/bl_types/bl_curve.py @@ -137,9 +137,6 @@ SPLINE_METADATA = [ class BlCurve(BlDatablock): bl_id = "curves" bl_class = bpy.types.Curve - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'CURVE_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index 4d79d5d..b7cc450 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -106,9 +106,6 @@ class BlDatablock(ReplicatedDatablock): bl_id : blender internal storage identifier bl_class : blender internal type - bl_delay_refresh : refresh rate in second for observers - bl_delay_apply : refresh rate in sec for apply - bl_automatic_push : boolean bl_icon : type icon (blender icon name) bl_check_common: enable check even in common rights bl_reload_parent: reload parent diff --git a/multi_user/bl_types/bl_file.py b/multi_user/bl_types/bl_file.py index f39106e..5801306 100644 --- a/multi_user/bl_types/bl_file.py +++ b/multi_user/bl_types/bl_file.py @@ -54,9 +54,6 @@ class BlFile(ReplicatedDatablock): bl_id = 'file' bl_name = "file" bl_class = Path - bl_delay_refresh = 2 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'FILE' bl_reload_parent = True diff --git a/multi_user/bl_types/bl_font.py b/multi_user/bl_types/bl_font.py index 0f3a532..c10ba10 100644 --- a/multi_user/bl_types/bl_font.py +++ b/multi_user/bl_types/bl_font.py @@ -30,9 +30,6 @@ from .dump_anything import Dumper, Loader class BlFont(BlDatablock): bl_id = "fonts" bl_class = bpy.types.VectorFont - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'FILE_FONT' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_gpencil.py b/multi_user/bl_types/bl_gpencil.py index bcc1296..02b55d6 100644 --- a/multi_user/bl_types/bl_gpencil.py +++ b/multi_user/bl_types/bl_gpencil.py @@ -231,9 +231,6 @@ def load_layer(layer_data, layer): class BlGpencil(BlDatablock): bl_id = "grease_pencils" bl_class = bpy.types.GreasePencil - bl_delay_refresh = 2 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'GREASEPENCIL' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_image.py b/multi_user/bl_types/bl_image.py index 6435551..c559938 100644 --- a/multi_user/bl_types/bl_image.py +++ b/multi_user/bl_types/bl_image.py @@ -51,9 +51,6 @@ format_to_ext = { class BlImage(BlDatablock): bl_id = "images" bl_class = bpy.types.Image - bl_delay_refresh = 2 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'IMAGE_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_lattice.py b/multi_user/bl_types/bl_lattice.py index ecb527d..64560ac 100644 --- a/multi_user/bl_types/bl_lattice.py +++ b/multi_user/bl_types/bl_lattice.py @@ -29,9 +29,6 @@ POINT = ['co', 'weight_softbody', 'co_deform'] class BlLattice(BlDatablock): bl_id = "lattices" bl_class = bpy.types.Lattice - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'LATTICE_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_library.py b/multi_user/bl_types/bl_library.py index 7e4b837..33a111e 100644 --- a/multi_user/bl_types/bl_library.py +++ b/multi_user/bl_types/bl_library.py @@ -26,9 +26,6 @@ from .bl_datablock import BlDatablock class BlLibrary(BlDatablock): bl_id = "libraries" bl_class = bpy.types.Library - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'LIBRARY_DATA_DIRECT' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_light.py b/multi_user/bl_types/bl_light.py index c0b7530..2bb1095 100644 --- a/multi_user/bl_types/bl_light.py +++ b/multi_user/bl_types/bl_light.py @@ -26,9 +26,6 @@ from .bl_datablock import BlDatablock class BlLight(BlDatablock): bl_id = "lights" bl_class = bpy.types.Light - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'LIGHT_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_lightprobe.py b/multi_user/bl_types/bl_lightprobe.py index 00b72f3..61052d0 100644 --- a/multi_user/bl_types/bl_lightprobe.py +++ b/multi_user/bl_types/bl_lightprobe.py @@ -27,9 +27,6 @@ from .bl_datablock import BlDatablock class BlLightprobe(BlDatablock): bl_id = "lightprobes" bl_class = bpy.types.LightProbe - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'LIGHTPROBE_GRID' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index c2a42b5..8e62ed2 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -364,9 +364,6 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_ class BlMaterial(BlDatablock): bl_id = "materials" bl_class = bpy.types.Material - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'MATERIAL_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_mesh.py b/multi_user/bl_types/bl_mesh.py index 8e6afbb..9ffda41 100644 --- a/multi_user/bl_types/bl_mesh.py +++ b/multi_user/bl_types/bl_mesh.py @@ -50,9 +50,6 @@ POLYGON = [ class BlMesh(BlDatablock): bl_id = "meshes" bl_class = bpy.types.Mesh - bl_delay_refresh = 2 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'MESH_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_metaball.py b/multi_user/bl_types/bl_metaball.py index 4cc146c..2a156a7 100644 --- a/multi_user/bl_types/bl_metaball.py +++ b/multi_user/bl_types/bl_metaball.py @@ -65,9 +65,6 @@ def load_metaball_elements(elements_data, elements): class BlMetaball(BlDatablock): bl_id = "metaballs" bl_class = bpy.types.MetaBall - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'META_BALL' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_node_group.py b/multi_user/bl_types/bl_node_group.py index 1d38969..a353659 100644 --- a/multi_user/bl_types/bl_node_group.py +++ b/multi_user/bl_types/bl_node_group.py @@ -28,9 +28,6 @@ from .bl_material import (dump_shader_node_tree, class BlNodeGroup(BlDatablock): bl_id = "node_groups" bl_class = bpy.types.ShaderNodeTree - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'NODETREE' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 281cd08..dceb7f6 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -107,9 +107,6 @@ def find_textures_dependencies(collection): class BlObject(BlDatablock): bl_id = "objects" bl_class = bpy.types.Object - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'OBJECT_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index 3ad8774..08a3d69 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -370,9 +370,6 @@ def load_sequence(sequence_data: dict, sequence_editor: bpy.types.SequenceEditor class BlScene(BlDatablock): bl_id = "scenes" bl_class = bpy.types.Scene - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = True bl_icon = 'SCENE_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_sound.py b/multi_user/bl_types/bl_sound.py index 514b2a9..b81a0b4 100644 --- a/multi_user/bl_types/bl_sound.py +++ b/multi_user/bl_types/bl_sound.py @@ -30,9 +30,6 @@ from .dump_anything import Dumper, Loader class BlSound(BlDatablock): bl_id = "sounds" bl_class = bpy.types.Sound - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'SOUND' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_speaker.py b/multi_user/bl_types/bl_speaker.py index 037bc05..c5f7b6c 100644 --- a/multi_user/bl_types/bl_speaker.py +++ b/multi_user/bl_types/bl_speaker.py @@ -26,9 +26,6 @@ from .bl_datablock import BlDatablock class BlSpeaker(BlDatablock): bl_id = "speakers" bl_class = bpy.types.Speaker - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'SPEAKER' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_texture.py b/multi_user/bl_types/bl_texture.py index 98d7898..f132e15 100644 --- a/multi_user/bl_types/bl_texture.py +++ b/multi_user/bl_types/bl_texture.py @@ -26,9 +26,6 @@ from .bl_datablock import BlDatablock class BlTexture(BlDatablock): bl_id = "textures" bl_class = bpy.types.Texture - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'TEXTURE' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_volume.py b/multi_user/bl_types/bl_volume.py index 93b490c..90a5a56 100644 --- a/multi_user/bl_types/bl_volume.py +++ b/multi_user/bl_types/bl_volume.py @@ -27,9 +27,6 @@ from .bl_material import dump_materials_slots, load_materials_slots class BlVolume(BlDatablock): bl_id = "volumes" bl_class = bpy.types.Volume - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = False bl_icon = 'VOLUME_DATA' bl_reload_parent = False diff --git a/multi_user/bl_types/bl_world.py b/multi_user/bl_types/bl_world.py index eba2959..ee0f15d 100644 --- a/multi_user/bl_types/bl_world.py +++ b/multi_user/bl_types/bl_world.py @@ -29,9 +29,6 @@ from .bl_material import (load_shader_node_tree, class BlWorld(BlDatablock): bl_id = "worlds" bl_class = bpy.types.World - bl_delay_refresh = 1 - bl_delay_apply = 1 - bl_automatic_push = True bl_check_common = True bl_icon = 'WORLD_DATA' bl_reload_parent = False diff --git a/multi_user/preferences.py b/multi_user/preferences.py index e40fc9e..1757c1b 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -97,8 +97,6 @@ def get_log_level(self): class ReplicatedDatablock(bpy.types.PropertyGroup): type_name: bpy.props.StringProperty() bl_name: bpy.props.StringProperty() - bl_delay_refresh: bpy.props.FloatProperty() - bl_delay_apply: bpy.props.FloatProperty() use_as_filter: bpy.props.BoolProperty(default=True) auto_push: bpy.props.BoolProperty(default=True) icon: bpy.props.StringProperty() @@ -273,11 +271,6 @@ class SessionPrefs(bpy.types.AddonPreferences): description="Rights", default=False ) - conf_session_timing_expanded: bpy.props.BoolProperty( - name="timings", - description="timings", - default=False - ) conf_session_cache_expanded: bpy.props.BoolProperty( name="Cache", description="cache", @@ -382,24 +375,6 @@ class SessionPrefs(bpy.types.AddonPreferences): row.label(text="Init the session from:") row.prop(self, "init_method", text="") - table = box.box() - table.row().prop( - self, "conf_session_timing_expanded", text="Refresh rates", - icon=get_expanded_icon(self.conf_session_timing_expanded), - emboss=False) - - if self.conf_session_timing_expanded: - line = table.row() - line.label(text=" ") - line.separator() - line.label(text="refresh (sec)") - line.label(text="apply (sec)") - - for item in self.supported_datablocks: - line = table.row(align=True) - line.label(text="", icon=item.icon) - line.prop(item, "bl_delay_refresh", text="") - line.prop(item, "bl_delay_apply", text="") # HOST SETTINGS box = grid.box() box.prop( @@ -455,11 +430,8 @@ class SessionPrefs(bpy.types.AddonPreferences): type_module_class = getattr(type_module, type_impl_name) new_db.name = type_impl_name new_db.type_name = type_impl_name - new_db.bl_delay_refresh = type_module_class.bl_delay_refresh - new_db.bl_delay_apply = type_module_class.bl_delay_apply new_db.use_as_filter = True new_db.icon = type_module_class.bl_icon - new_db.auto_push = type_module_class.bl_automatic_push new_db.bl_name = type_module_class.bl_id diff --git a/multi_user/timers.py b/multi_user/timers.py index 60725f3..9e1d2e6 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -101,17 +101,9 @@ class SessionBackupTimer(Timer): session.save(self._filepath) class ApplyTimer(Timer): - def __init__(self, timeout=1, target_type=None): - self._type = target_type - super().__init__(timeout) - self.id = target_type.__name__ if target_type else "ApplyTimer" - def execute(self): if session and session.state['STATE'] == STATE_ACTIVE: - if self._type: - nodes = session.list(filter=self._type) - else: - nodes = session.list() + nodes = session.list() for node in nodes: node_ref = session.get(uuid=node) @@ -122,13 +114,10 @@ class ApplyTimer(Timer): except Exception as e: logging.error(f"Fail to apply {node_ref.uuid}: {e}") else: - if self._type.bl_reload_parent: - parents = [] - - for n in session.list(): - deps = session.get(uuid=n).dependencies - if deps and node in deps: - session.apply(n, force=True) + if node_ref.bl_reload_parent: + for parent in session._graph.find_parents(node): + logging.debug("Refresh parent {node}") + session.apply(parent, force=True) class DynamicRightSelectTimer(Timer): diff --git a/multi_user/ui.py b/multi_user/ui.py index ea575a6..b8f423b 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -281,7 +281,7 @@ class SESSION_PT_advanced_settings(bpy.types.Panel): warning = replication_section_row.box() warning.label(text="Don't use this with heavy meshes !", icon='ERROR') replication_section_row = replication_section.row() - replication_section_row.prop(settings, "depsgraph_update_rate") + replication_section_row.prop(settings, "depsgraph_update_rate", text="Apply delay") cache_section = layout.row().box()