diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 9da144e..0d5d9a2 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.16'), + ("replication", '0.1.17'), } diff --git a/multi_user/bl_types/bl_action.py b/multi_user/bl_types/bl_action.py index 253a13e..0bee448 100644 --- a/multi_user/bl_types/bl_action.py +++ b/multi_user/bl_types/bl_action.py @@ -137,6 +137,7 @@ class BlAction(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'ACTION_TWEAK' + bl_reload_parent = False def _construct(self, data): return bpy.data.actions.new(data["name"]) diff --git a/multi_user/bl_types/bl_armature.py b/multi_user/bl_types/bl_armature.py index 21cfe31..8de002f 100644 --- a/multi_user/bl_types/bl_armature.py +++ b/multi_user/bl_types/bl_armature.py @@ -33,6 +33,7 @@ class BlArmature(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'ARMATURE_DATA' + bl_reload_parent = False def _construct(self, data): return bpy.data.armatures.new(data["name"]) diff --git a/multi_user/bl_types/bl_camera.py b/multi_user/bl_types/bl_camera.py index 22f58ae..78b9968 100644 --- a/multi_user/bl_types/bl_camera.py +++ b/multi_user/bl_types/bl_camera.py @@ -31,6 +31,7 @@ class BlCamera(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'CAMERA_DATA' + bl_reload_parent = False def _construct(self, data): return bpy.data.cameras.new(data["name"]) diff --git a/multi_user/bl_types/bl_collection.py b/multi_user/bl_types/bl_collection.py index 12b4948..7b78989 100644 --- a/multi_user/bl_types/bl_collection.py +++ b/multi_user/bl_types/bl_collection.py @@ -89,6 +89,7 @@ class BlCollection(BlDatablock): bl_delay_apply = 1 bl_automatic_push = True bl_check_common = True + bl_reload_parent = False def _construct(self, data): if self.is_library: diff --git a/multi_user/bl_types/bl_curve.py b/multi_user/bl_types/bl_curve.py index 4f5dffa..00fd0c8 100644 --- a/multi_user/bl_types/bl_curve.py +++ b/multi_user/bl_types/bl_curve.py @@ -146,6 +146,7 @@ class BlCurve(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'CURVE_DATA' + bl_reload_parent = False def _construct(self, data): return bpy.data.curves.new(data["name"], data["type"]) diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index c75fc20..836a4a2 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -111,8 +111,10 @@ class BlDatablock(ReplicatedDatablock): 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 """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) instance = kwargs.get('instance', None) diff --git a/multi_user/bl_types/bl_file.py b/multi_user/bl_types/bl_file.py index c5b64ed..1dd8919 100644 --- a/multi_user/bl_types/bl_file.py +++ b/multi_user/bl_types/bl_file.py @@ -54,11 +54,12 @@ class BlFile(ReplicatedDatablock): bl_id = 'file' bl_name = "file" bl_class = Path - bl_delay_refresh = 0 + bl_delay_refresh = 2 bl_delay_apply = 1 bl_automatic_push = True bl_check_common = False bl_icon = 'FILE' + bl_reload_parent = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -118,11 +119,7 @@ class BlFile(ReplicatedDatablock): """ Writing the file """ - # TODO: check for empty data - if target.exists() and not self.diff(): - logging.info(f"{data['name']} already on the disk, skipping.") - return try: file = open(target, "wb") file.write(data['file']) @@ -140,4 +137,4 @@ class BlFile(ReplicatedDatablock): else: memory_size = sys.getsizeof(self.data['file'])-33 disk_size = self.instance.stat().st_size - return memory_size == disk_size + return memory_size != disk_size diff --git a/multi_user/bl_types/bl_font.py b/multi_user/bl_types/bl_font.py index 58062ab..0f3a532 100644 --- a/multi_user/bl_types/bl_font.py +++ b/multi_user/bl_types/bl_font.py @@ -35,6 +35,7 @@ class BlFont(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'FILE_FONT' + bl_reload_parent = False def _construct(self, data): filename = data.get('filename') diff --git a/multi_user/bl_types/bl_gpencil.py b/multi_user/bl_types/bl_gpencil.py index e440788..be6b649 100644 --- a/multi_user/bl_types/bl_gpencil.py +++ b/multi_user/bl_types/bl_gpencil.py @@ -235,6 +235,7 @@ class BlGpencil(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'GREASEPENCIL' + bl_reload_parent = False def _construct(self, data): return bpy.data.grease_pencils.new(data["name"]) diff --git a/multi_user/bl_types/bl_image.py b/multi_user/bl_types/bl_image.py index 0a0b61c..6435551 100644 --- a/multi_user/bl_types/bl_image.py +++ b/multi_user/bl_types/bl_image.py @@ -51,11 +51,12 @@ format_to_ext = { class BlImage(BlDatablock): bl_id = "images" bl_class = bpy.types.Image - bl_delay_refresh = 1 + bl_delay_refresh = 2 bl_delay_apply = 1 bl_automatic_push = True bl_check_common = False bl_icon = 'IMAGE_DATA' + bl_reload_parent = False def _construct(self, data): return bpy.data.images.new( @@ -71,7 +72,6 @@ class BlImage(BlDatablock): target.source = 'FILE' target.filepath_raw = get_filepath(data['filename']) target.colorspace_settings.name = data["colorspace_settings"]["name"] - def _dump(self, instance=None): assert(instance) @@ -96,6 +96,9 @@ class BlImage(BlDatablock): return data def diff(self): + if self.instance.is_dirty: + self.instance.save() + if self.instance and (self.instance.name != self.data['name']): return True else: @@ -103,21 +106,21 @@ class BlImage(BlDatablock): def _resolve_deps_implementation(self): deps = [] + + if self.instance.packed_file: + filename = Path(bpy.path.abspath(self.instance.filepath)).name + self.instance.filepath_raw = get_filepath(filename) + self.instance.save() + # An image can't be unpacked to the modified path + # TODO: make a bug report + self.instance.unpack(method="REMOVE") + + elif self.instance.source == "GENERATED": + filename = f"{self.instance.name}.png" + self.instance.filepath = get_filepath(filename) + self.instance.save() + if self.instance.filepath: - - if self.instance.packed_file: - filename = Path(bpy.path.abspath(self.instance.filepath)).name - self.instance.filepath_raw = get_filepath(filename) - self.instance.save() - # An image can't be unpacked to the modified path - # TODO: make a bug report - self.instance.unpack(method="REMOVE") - - elif self.instance.source == "GENERATED": - filename = f"{self.instance.name}.png" - self.instance.filepath = get_filepath(filename) - self.instance.save() - deps.append(Path(bpy.path.abspath(self.instance.filepath))) return deps diff --git a/multi_user/bl_types/bl_lattice.py b/multi_user/bl_types/bl_lattice.py index 1791eb3..ecb527d 100644 --- a/multi_user/bl_types/bl_lattice.py +++ b/multi_user/bl_types/bl_lattice.py @@ -34,6 +34,7 @@ class BlLattice(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'LATTICE_DATA' + bl_reload_parent = False def _construct(self, data): return bpy.data.lattices.new(data["name"]) diff --git a/multi_user/bl_types/bl_library.py b/multi_user/bl_types/bl_library.py index ba16abe..7e4b837 100644 --- a/multi_user/bl_types/bl_library.py +++ b/multi_user/bl_types/bl_library.py @@ -31,6 +31,7 @@ class BlLibrary(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'LIBRARY_DATA_DIRECT' + bl_reload_parent = False def _construct(self, data): with bpy.data.libraries.load(filepath=data["filepath"], link=True) as (sourceData, targetData): diff --git a/multi_user/bl_types/bl_light.py b/multi_user/bl_types/bl_light.py index 810bf5c..c0b7530 100644 --- a/multi_user/bl_types/bl_light.py +++ b/multi_user/bl_types/bl_light.py @@ -31,6 +31,7 @@ class BlLight(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'LIGHT_DATA' + bl_reload_parent = False def _construct(self, data): return bpy.data.lights.new(data["name"], data["type"]) diff --git a/multi_user/bl_types/bl_lightprobe.py b/multi_user/bl_types/bl_lightprobe.py index 28d5f93..00b72f3 100644 --- a/multi_user/bl_types/bl_lightprobe.py +++ b/multi_user/bl_types/bl_lightprobe.py @@ -32,6 +32,7 @@ class BlLightprobe(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'LIGHTPROBE_GRID' + bl_reload_parent = False def _construct(self, data): type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type'] diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index ce18d90..90424b2 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -327,6 +327,7 @@ class BlMaterial(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'MATERIAL_DATA' + bl_reload_parent = False def _construct(self, data): return bpy.data.materials.new(data["name"]) diff --git a/multi_user/bl_types/bl_mesh.py b/multi_user/bl_types/bl_mesh.py index 70546b7..26611e6 100644 --- a/multi_user/bl_types/bl_mesh.py +++ b/multi_user/bl_types/bl_mesh.py @@ -54,6 +54,7 @@ class BlMesh(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'MESH_DATA' + bl_reload_parent = False def _construct(self, data): instance = bpy.data.meshes.new(data["name"]) diff --git a/multi_user/bl_types/bl_metaball.py b/multi_user/bl_types/bl_metaball.py index 4d5f2e0..4cc146c 100644 --- a/multi_user/bl_types/bl_metaball.py +++ b/multi_user/bl_types/bl_metaball.py @@ -70,6 +70,7 @@ class BlMetaball(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'META_BALL' + bl_reload_parent = False def _construct(self, data): return bpy.data.metaballs.new(data["name"]) diff --git a/multi_user/bl_types/bl_node_group.py b/multi_user/bl_types/bl_node_group.py index 8ebf568..1d38969 100644 --- a/multi_user/bl_types/bl_node_group.py +++ b/multi_user/bl_types/bl_node_group.py @@ -33,6 +33,7 @@ class BlNodeGroup(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'NODETREE' + bl_reload_parent = False def _construct(self, data): return bpy.data.node_groups.new(data["name"], data["type"]) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 2661e0e..7a5c2bb 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -102,6 +102,7 @@ class BlObject(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'OBJECT_DATA' + bl_reload_parent = False def _construct(self, data): instance = None diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index 02f0c89..575f515 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -278,6 +278,7 @@ class BlScene(BlDatablock): bl_automatic_push = True bl_check_common = True bl_icon = 'SCENE_DATA' + bl_reload_parent = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/multi_user/bl_types/bl_sequencer.py b/multi_user/bl_types/bl_sequencer.py index b2376fd..6d74aa3 100644 --- a/multi_user/bl_types/bl_sequencer.py +++ b/multi_user/bl_types/bl_sequencer.py @@ -130,6 +130,7 @@ class BlSequencer(BlDatablock): bl_automatic_push = True bl_check_common = True bl_icon = 'SEQUENCE' + bl_reload_parent = False def _construct(self, data): # Get the scene diff --git a/multi_user/bl_types/bl_sound.py b/multi_user/bl_types/bl_sound.py index 7927292..514b2a9 100644 --- a/multi_user/bl_types/bl_sound.py +++ b/multi_user/bl_types/bl_sound.py @@ -35,6 +35,7 @@ class BlSound(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'SOUND' + bl_reload_parent = False def _construct(self, data): filename = data.get('filename') diff --git a/multi_user/bl_types/bl_speaker.py b/multi_user/bl_types/bl_speaker.py index af2ef28..037bc05 100644 --- a/multi_user/bl_types/bl_speaker.py +++ b/multi_user/bl_types/bl_speaker.py @@ -31,6 +31,7 @@ class BlSpeaker(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'SPEAKER' + bl_reload_parent = False def _load_implementation(self, data, target): loader = Loader() diff --git a/multi_user/bl_types/bl_texture.py b/multi_user/bl_types/bl_texture.py index ee19e26..98d7898 100644 --- a/multi_user/bl_types/bl_texture.py +++ b/multi_user/bl_types/bl_texture.py @@ -31,6 +31,7 @@ class BlTexture(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'TEXTURE' + bl_reload_parent = False def _load_implementation(self, data, target): loader = Loader() diff --git a/multi_user/bl_types/bl_volume.py b/multi_user/bl_types/bl_volume.py index 2e15dc2..377bdb9 100644 --- a/multi_user/bl_types/bl_volume.py +++ b/multi_user/bl_types/bl_volume.py @@ -32,6 +32,7 @@ class BlVolume(BlDatablock): bl_automatic_push = True bl_check_common = False bl_icon = 'VOLUME_DATA' + bl_reload_parent = False def _load_implementation(self, data, target): loader = Loader() diff --git a/multi_user/bl_types/bl_world.py b/multi_user/bl_types/bl_world.py index 99ba1ae..eba2959 100644 --- a/multi_user/bl_types/bl_world.py +++ b/multi_user/bl_types/bl_world.py @@ -34,6 +34,7 @@ class BlWorld(BlDatablock): bl_automatic_push = True bl_check_common = True bl_icon = 'WORLD_DATA' + bl_reload_parent = False def _construct(self, data): return bpy.data.worlds.new(data["name"]) diff --git a/multi_user/delayable.py b/multi_user/delayable.py index 5e120fc..3196b98 100644 --- a/multi_user/delayable.py +++ b/multi_user/delayable.py @@ -28,6 +28,7 @@ from .presence import (renderer, generate_user_camera, get_view_matrix, refresh_sidebar_view) +from . import operators from replication.constants import (FETCHED, UP, RP_COMMON, @@ -41,18 +42,16 @@ from replication.constants import (FETCHED, from replication.interface import session from replication.exception import NonAuthorizedOperationError -def is_annotating(context:bpy.types.Context): + +def is_annotating(context: bpy.types.Context): """ Check if the annotate mode is enabled """ - return bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create=False).idname == 'builtin.annotate' + return bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create=False).idname == 'builtin.annotate' class Delayable(): """Delayable task interface """ - def __init__(self): - self.is_registered = False - def register(self): raise NotImplementedError @@ -72,25 +71,30 @@ class Timer(Delayable): def __init__(self, duration=1): super().__init__() self._timeout = duration - self._running = True + self.is_running = False def register(self): """Register the timer into the blender timer system """ - if not self.is_registered: + if not self.is_running: bpy.app.timers.register(self.main) - self.is_registered = True + self.is_running = True logging.debug(f"Register {self.__class__.__name__}") else: logging.debug( f"Timer {self.__class__.__name__} already registered") def main(self): - self.execute() - - if self._running: - return self._timeout + try: + self.execute() + except Exception as e: + logging.error(e) + self.unregister() + session.disconnect() + else: + if self.is_running: + return self._timeout def execute(self): """Main timer loop @@ -103,7 +107,7 @@ class Timer(Delayable): if bpy.app.timers.is_registered(self.main): bpy.app.timers.unregister(self.main) - self._running = False + self.is_running = False class ApplyTimer(Timer): @@ -126,14 +130,20 @@ class ApplyTimer(Timer): session.apply(node) 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) class DynamicRightSelectTimer(Timer): def __init__(self, timout=.1): super().__init__(timout) self._last_selection = [] self._user = None - self._right_strategy = RP_COMMON self._annotating = False def execute(self): @@ -146,27 +156,28 @@ class DynamicRightSelectTimer(Timer): if self._user: ctx = bpy.context - annotation_gp = ctx.scene.grease_pencil + annotation_gp = ctx.scene.grease_pencil # if an annotation exist and is tracked if annotation_gp and annotation_gp.uuid: registered_gp = session.get(uuid=annotation_gp.uuid) - if is_annotating(bpy.context): + if is_annotating(bpy.context): # try to get the right on it if registered_gp.owner == RP_COMMON: self._annotating = True - logging.debug("Getting the right on the annotation GP") + logging.debug( + "Getting the right on the annotation GP") session.change_owner( - registered_gp.uuid, - settings.username, - ignore_warnings=True, - affect_dependencies=False) - elif self._annotating: + registered_gp.uuid, + settings.username, + ignore_warnings=True, + affect_dependencies=False) + elif self._annotating: session.change_owner( - registered_gp.uuid, - RP_COMMON, - ignore_warnings=True, - affect_dependencies=False) + registered_gp.uuid, + RP_COMMON, + ignore_warnings=True, + affect_dependencies=False) current_selection = utils.get_selected_objects( bpy.context.scene, @@ -193,7 +204,8 @@ class DynamicRightSelectTimer(Timer): ignore_warnings=True, affect_dependencies=recursive) except NonAuthorizedOperationError: - logging.warning(f"Not authorized to change {node} owner") + logging.warning( + f"Not authorized to change {node} owner") # change new selection to our for obj in obj_ours: @@ -211,7 +223,8 @@ class DynamicRightSelectTimer(Timer): ignore_warnings=True, affect_dependencies=recursive) except NonAuthorizedOperationError: - logging.warning(f"Not authorized to change {node} owner") + logging.warning( + f"Not authorized to change {node} owner") else: return @@ -225,7 +238,7 @@ class DynamicRightSelectTimer(Timer): logging.debug("Update selection") # Fix deselection until right managment refactoring (with Roles concepts) - if len(current_selection) == 0 and self._right_strategy == RP_COMMON: + if len(current_selection) == 0 : owned_keys = session.list( filter_owner=settings.username) for key in owned_keys: @@ -237,7 +250,8 @@ class DynamicRightSelectTimer(Timer): ignore_warnings=True, affect_dependencies=recursive) except NonAuthorizedOperationError: - logging.warning(f"Not authorized to change {key} owner") + logging.warning( + f"Not authorized to change {key} owner") for obj in bpy.data.objects: object_uuid = getattr(obj, 'uuid', None) @@ -246,6 +260,7 @@ class DynamicRightSelectTimer(Timer): if obj.hide_select != is_selectable: obj.hide_select = is_selectable + class ClientUpdate(Timer): def __init__(self, timout=.1): super().__init__(timout) @@ -360,4 +375,4 @@ class MainThreadExecutor(Timer): while not self.execution_queue.empty(): function, kwargs = self.execution_queue.get() logging.debug(f"Executing {function.__name__}") - function(**kwargs) + function(**kwargs) \ No newline at end of file diff --git a/multi_user/operators.py b/multi_user/operators.py index ba2e683..5dbbd7d 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -42,7 +42,7 @@ from . import bl_types, delayable, environment, ui, utils from .presence import SessionStatusWidget, renderer, view3d_find background_execution_queue = Queue() -delayables = [] +deleyables = [] stop_modal_executor = False @@ -80,7 +80,7 @@ def initialize_session(): node_ref.apply() # Step 4: Register blender timers - for d in delayables: + for d in deleyables: d.register() if settings.update_method == 'DEPSGRAPH': @@ -93,15 +93,16 @@ def initialize_session(): def on_connection_end(reason="none"): """Session connection finished handler """ - global delayables, stop_modal_executor + global deleyables, stop_modal_executor settings = utils.get_preferences() # Step 1: Unregister blender timers - for d in delayables: + for d in deleyables: try: d.unregister() except: continue + deleyables.clear() stop_modal_executor = True @@ -131,7 +132,7 @@ class SessionStartOperator(bpy.types.Operator): return True def execute(self, context): - global delayables + global deleyables settings = utils.get_preferences() runtime_settings = context.window_manager.session @@ -139,7 +140,7 @@ class SessionStartOperator(bpy.types.Operator): admin_pass = runtime_settings.password use_extern_update = settings.update_method == 'DEPSGRAPH' users.clear() - delayables.clear() + deleyables.clear() logger = logging.getLogger() if len(logger.handlers) == 1: @@ -191,7 +192,7 @@ class SessionStartOperator(bpy.types.Operator): if settings.update_method == 'DEFAULT': if type_local_config.bl_delay_apply > 0: - delayables.append( + deleyables.append( delayable.ApplyTimer( timout=type_local_config.bl_delay_apply, target_type=type_module_class)) @@ -207,7 +208,7 @@ class SessionStartOperator(bpy.types.Operator): external_update_handling=use_extern_update) if settings.update_method == 'DEPSGRAPH': - delayables.append(delayable.ApplyTimer( + deleyables.append(delayable.ApplyTimer( settings.depsgraph_update_rate/1000)) # Host a session @@ -258,8 +259,8 @@ class SessionStartOperator(bpy.types.Operator): logging.error(str(e)) # Background client updates service - delayables.append(delayable.ClientUpdate()) - delayables.append(delayable.DynamicRightSelectTimer()) + deleyables.append(delayable.ClientUpdate()) + deleyables.append(delayable.DynamicRightSelectTimer()) session_update = delayable.SessionStatusUpdate() session_user_sync = delayable.SessionUserSync() @@ -270,9 +271,9 @@ class SessionStartOperator(bpy.types.Operator): session_user_sync.register() session_background_executor.register() - delayables.append(session_background_executor) - delayables.append(session_update) - delayables.append(session_user_sync) + deleyables.append(session_background_executor) + deleyables.append(session_update) + deleyables.append(session_user_sync) @@ -332,7 +333,7 @@ class SessionStopOperator(bpy.types.Operator): return True def execute(self, context): - global delayables, stop_modal_executor + global deleyables, stop_modal_executor if session: try: @@ -359,7 +360,7 @@ class SessionKickOperator(bpy.types.Operator): return True def execute(self, context): - global delayables, stop_modal_executor + global deleyables, stop_modal_executor assert(session) try: