diff --git a/README.md b/README.md index fdc5a78..80525cb 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ Currently, not all data-block are supported for replication over the wire. The f | image | ✔️ | | | mesh | ✔️ | | | material | ✔️ | | -| node_groups | ❗ | Material only | | metaball | ✔️ | | | object | ✔️ | | | texts | ✔️ | | @@ -49,7 +48,7 @@ Currently, not all data-block are supported for replication over the wire. The f | volumes | ❌ | | | particles | ❌ | [On-going](https://gitlab.com/slumber/multi-user/-/issues/24) | | speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) | -| vse | ❗ | Mask and Clip not supported yet | +| vse | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) | | physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) | | libraries | ❗ | Partial | diff --git a/docs/getting_started/img/quickstart_presence.png b/docs/getting_started/img/quickstart_presence.png index cc39bb0..771ca9f 100644 Binary files a/docs/getting_started/img/quickstart_presence.png and b/docs/getting_started/img/quickstart_presence.png differ diff --git a/docs/getting_started/img/quickstart_status.png b/docs/getting_started/img/quickstart_status.png deleted file mode 100644 index 0a66d56..0000000 Binary files a/docs/getting_started/img/quickstart_status.png and /dev/null differ diff --git a/docs/getting_started/quickstart.rst b/docs/getting_started/quickstart.rst index dae7346..2f99140 100644 --- a/docs/getting_started/quickstart.rst +++ b/docs/getting_started/quickstart.rst @@ -251,14 +251,6 @@ it draw users related information in your viewport such as: The presence overlay panel (see image above) allow you to enable/disable various drawn parts via the following flags: -- **Show session statut**: display the session status in the viewport - - .. figure:: img/quickstart_status.png - :align: center - - - **Text scale**: session status text size - - **Vertical/Horizontal position**: session position in the viewport - - **Show selected objects**: display other users current selection - **Show users**: display users current viewpoint - **Show different scenes**: display users working on other scenes diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 389d4c8..41e248b 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, 1, 1), "description": "Enable real-time collaborative workflow inside blender", "blender": (2, 82, 0), "location": "3D View > Sidebar > Multi-User tab", @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.9'), + ("replication", '0.1.3'), } diff --git a/multi_user/bl_types/__init__.py b/multi_user/bl_types/__init__.py index 07a07f1..add7058 100644 --- a/multi_user/bl_types/__init__.py +++ b/multi_user/bl_types/__init__.py @@ -37,9 +37,7 @@ __all__ = [ 'bl_speaker', 'bl_font', 'bl_sound', - 'bl_file', - 'bl_sequencer', - 'bl_node_group' + 'bl_file' ] # Order here defines execution order from . import * diff --git a/multi_user/bl_types/bl_action.py b/multi_user/bl_types/bl_action.py index 253a13e..15d3622 100644 --- a/multi_user/bl_types/bl_action.py +++ b/multi_user/bl_types/bl_action.py @@ -42,7 +42,7 @@ KEYFRAME = [ ] -def dump_fcurve(fcurve: bpy.types.FCurve, use_numpy: bool = True) -> dict: +def dump_fcurve(fcurve: bpy.types.FCurve, use_numpy:bool =True) -> dict: """ Dump a sigle curve to a dict :arg fcurve: fcurve to dump @@ -59,7 +59,7 @@ def dump_fcurve(fcurve: bpy.types.FCurve, use_numpy: bool = True) -> dict: if use_numpy: points = fcurve.keyframe_points - fcurve_data['keyframes_count'] = len(fcurve.keyframe_points) + fcurve_data['keyframes_count'] = len(fcurve.keyframe_points) fcurve_data['keyframe_points'] = np_dump_collection(points, KEYFRAME) else: # Legacy method @@ -92,8 +92,7 @@ def load_fcurve(fcurve_data, fcurve): if use_numpy: keyframe_points.add(fcurve_data['keyframes_count']) - np_load_collection( - fcurve_data["keyframe_points"], keyframe_points, KEYFRAME) + np_load_collection(fcurve_data["keyframe_points"], keyframe_points, KEYFRAME) else: # paste dumped keyframes @@ -154,11 +153,7 @@ class BlAction(BlDatablock): dumped_data_path, index=dumped_array_index) load_fcurve(dumped_fcurve, fcurve) - - id_root = data.get('id_root') - - if id_root: - target.id_root = id_root + target.id_root = data['id_root'] def _dump_implementation(self, data, instance=None): dumper = Dumper() diff --git a/multi_user/bl_types/bl_camera.py b/multi_user/bl_types/bl_camera.py index 22f58ae..e65b85a 100644 --- a/multi_user/bl_types/bl_camera.py +++ b/multi_user/bl_types/bl_camera.py @@ -48,15 +48,12 @@ class BlCamera(BlDatablock): background_images = data.get('background_images') - target.background_images.clear() - if background_images: + target.background_images.clear() for img_name, img_data in background_images.items(): - img_id = img_data.get('image') - if img_id: - target_img = target.background_images.new() - target_img.image = bpy.data.images[img_id] - loader.load(target_img, img_data) + target_img = target.background_images.new() + target_img.image = bpy.data.images[img_name] + loader.load(target_img, img_data) def _dump_implementation(self, data, instance=None): assert(instance) diff --git a/multi_user/bl_types/bl_collection.py b/multi_user/bl_types/bl_collection.py index 12b4948..542f49f 100644 --- a/multi_user/bl_types/bl_collection.py +++ b/multi_user/bl_types/bl_collection.py @@ -71,15 +71,6 @@ def load_collection_childrens(dumped_childrens, collection): if child_collection.uuid not in dumped_childrens: collection.children.unlink(child_collection) -def resolve_collection_dependencies(collection): - deps = [] - - for child in collection.children: - deps.append(child) - for object in collection.objects: - deps.append(object) - - return deps class BlCollection(BlDatablock): bl_id = "collections" @@ -133,4 +124,11 @@ class BlCollection(BlDatablock): return data def _resolve_deps_implementation(self): - return resolve_collection_dependencies(self.instance) + deps = [] + + for child in self.instance.children: + deps.append(child) + for object in self.instance.objects: + deps.append(object) + + return deps diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index c75fc20..c7996e3 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -21,7 +21,7 @@ from collections.abc import Iterable import bpy import mathutils -from replication.constants import DIFF_BINARY, DIFF_JSON, UP +from replication.constants import DIFF_BINARY, UP from replication.data import ReplicatedDatablock from .. import utils @@ -92,6 +92,7 @@ def load_driver(target_datablock, src_driver): def get_datablock_from_uuid(uuid, default, ignore=[]): if not uuid: return default + for category in dir(bpy.data): root = getattr(bpy.data, category) if isinstance(root, Iterable) and category not in ignore: @@ -122,15 +123,12 @@ class BlDatablock(ReplicatedDatablock): # TODO: use is_library_indirect self.is_library = (instance and hasattr(instance, 'library') and instance.library) or \ - (hasattr(self,'data') and self.data and 'library' in self.data) + (self.data and 'library' in self.data) 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 + self.diff_method = DIFF_BINARY def resolve(self): datablock_ref = None @@ -219,7 +217,7 @@ class BlDatablock(ReplicatedDatablock): if not self.is_library: dependencies.extend(self._resolve_deps_implementation()) - logging.debug(f"{self.instance} dependencies: {dependencies}") + logging.debug(f"{self.instance.name} dependencies: {dependencies}") return dependencies def _resolve_deps_implementation(self): diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 080c515..81fc6b5 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -21,8 +21,6 @@ import mathutils import logging import re -from uuid import uuid4 - from .dump_anything import Loader, Dumper from .bl_datablock import BlDatablock, get_datablock_from_uuid @@ -39,34 +37,28 @@ def load_node(node_data, node_tree): """ loader = Loader() target_node = node_tree.nodes.new(type=node_data["bl_idname"]) - target_node.select = False + loader.load(target_node, node_data) image_uuid = node_data.get('image_uuid', None) - node_tree_uuid = node_data.get('node_tree_uuid', None) if image_uuid and not target_node.image: target_node.image = get_datablock_from_uuid(image_uuid, None) - if node_tree_uuid: - target_node.node_tree = get_datablock_from_uuid(node_tree_uuid, None) + for input in node_data["inputs"]: + if hasattr(target_node.inputs[input], "default_value"): + try: + target_node.inputs[input].default_value = node_data["inputs"][input]["default_value"] + except: + logging.error( + f"Material {input} parameter not supported, skipping") - inputs = node_data.get('inputs') - if inputs: - for idx, inpt in enumerate(inputs): - if hasattr(target_node.inputs[idx], "default_value"): - try: - target_node.inputs[idx].default_value = inpt["default_value"] - except: - logging.error(f"Material input {inpt.keys()} parameter not supported, skipping") - - outputs = node_data.get('outputs') - if outputs: - for idx, output in enumerate(outputs): - if hasattr(target_node.outputs[idx], "default_value"): - try: - target_node.outputs[idx].default_value = output["default_value"] - except: - logging.error(f"Material output {output.keys()} parameter not supported, skipping") + for output in node_data["outputs"]: + if hasattr(target_node.outputs[output], "default_value"): + try: + target_node.outputs[output].default_value = node_data["outputs"][output]["default_value"] + except: + logging.error( + f"Material {output} parameter not supported, skipping") def load_links(links_data, node_tree): @@ -150,20 +142,24 @@ def dump_node(node): dumped_node = node_dumper.dump(node) if hasattr(node, 'inputs'): - dumped_node['inputs'] = [] + dumped_node['inputs'] = {} - io_dumper = Dumper() - io_dumper.depth = 2 - io_dumper.include_filter = ["default_value"] + for i in node.inputs: + input_dumper = Dumper() + input_dumper.depth = 2 + input_dumper.include_filter = ["default_value"] - for idx, inpt in enumerate(node.inputs): - if hasattr(inpt, 'default_value'): - dumped_node['inputs'].append(io_dumper.dump(inpt)) + if hasattr(i, 'default_value'): + dumped_node['inputs'][i.name] = input_dumper.dump(i) - dumped_node['outputs'] = [] - for idx, output in enumerate(node.outputs): - if hasattr(output, 'default_value'): - dumped_node['outputs'].append(io_dumper.dump(output)) + dumped_node['outputs'] = {} + for i in node.outputs: + output_dumper = Dumper() + output_dumper.depth = 2 + output_dumper.include_filter = ["default_value"] + + if hasattr(i, 'default_value'): + dumped_node['outputs'][i.name] = output_dumper.dump(i) if hasattr(node, 'color_ramp'): ramp_dumper = Dumper() @@ -186,126 +182,13 @@ def dump_node(node): dumped_node['mapping'] = curve_dumper.dump(node.mapping) if hasattr(node, 'image') and getattr(node, 'image'): dumped_node['image_uuid'] = node.image.uuid - if hasattr(node, 'node_tree') and getattr(node, 'node_tree'): - dumped_node['node_tree_uuid'] = node.node_tree.uuid return dumped_node -def dump_shader_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict: - """ Dump a shader node_tree to a dict including links and nodes - - :arg node_tree: dumped shader node tree - :type node_tree: bpy.types.ShaderNodeTree - :return: dict - """ - node_tree_data = { - 'nodes': {node.name: dump_node(node) for node in node_tree.nodes}, - 'links': dump_links(node_tree.links), - 'name': node_tree.name, - 'type': type(node_tree).__name__ - } - - for socket_id in ['inputs', 'outputs']: - socket_collection = getattr(node_tree, socket_id) - node_tree_data[socket_id] = dump_node_tree_sockets(socket_collection) - - return node_tree_data - - -def dump_node_tree_sockets(sockets: bpy.types.Collection)->dict: - """ dump sockets of a shader_node_tree - - :arg target_node_tree: target node_tree - :type target_node_tree: bpy.types.NodeTree - :arg socket_id: socket identifer - :type socket_id: str - :return: dict - """ - sockets_data = [] - for socket in sockets: - try: - socket_uuid = socket['uuid'] - except Exception: - socket_uuid = str(uuid4()) - socket['uuid'] = socket_uuid - - sockets_data.append((socket.name, socket.bl_socket_idname, socket_uuid)) - - return sockets_data - -def load_node_tree_sockets(sockets: bpy.types.Collection, - sockets_data: dict): - """ load sockets of a shader_node_tree - - :arg target_node_tree: target node_tree - :type target_node_tree: bpy.types.NodeTree - :arg socket_id: socket identifer - :type socket_id: str - :arg socket_data: dumped socket data - :type socket_data: dict - """ - # Check for removed sockets - for socket in sockets: - if not [s for s in sockets_data if socket['uuid'] == s[2]]: - sockets.remove(socket) - - # Check for new sockets - for idx, socket_data in enumerate(sockets_data): - try: - checked_socket = sockets[idx] - if checked_socket.name != socket_data[0]: - checked_socket.name = socket_data[0] - except Exception: - s = sockets.new(socket_data[1], socket_data[0]) - s['uuid'] = socket_data[2] - - -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 - :type node_tree_data: dict - :arg target_node_tree: target node_tree - :type target_node_tree: bpy.types.NodeTree - """ - # TODO: load only required nodes - target_node_tree.nodes.clear() - - if not target_node_tree.is_property_readonly('name'): - target_node_tree.name = node_tree_data['name'] - - if 'inputs' in node_tree_data: - socket_collection = getattr(target_node_tree, 'inputs') - load_node_tree_sockets(socket_collection, node_tree_data['inputs']) - - if 'outputs' in node_tree_data: - socket_collection = getattr(target_node_tree, 'outputs') - load_node_tree_sockets(socket_collection,node_tree_data['outputs']) - - # Load nodes - for node in node_tree_data["nodes"]: - load_node(node_tree_data["nodes"][node], target_node_tree) - - # TODO: load only required nodes links - # Load nodes links - target_node_tree.links.clear() - - load_links(node_tree_data["links"], target_node_tree) - - 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) - deps = [] - - for node in node_tree.nodes: - if has_image(node): - deps.append(node.image) - elif has_node_group(node): - deps.append(node.node_tree) - - return deps + return [node.image for node in node_tree.nodes if has_image(node)] class BlMaterial(BlDatablock): @@ -336,7 +219,16 @@ class BlMaterial(BlDatablock): if target.node_tree is None: target.use_nodes = True - load_shader_node_tree(data['node_tree'], target.node_tree) + target.node_tree.nodes.clear() + + # Load nodes + for node in data["node_tree"]["nodes"]: + load_node(data["node_tree"]["nodes"][node], target.node_tree) + + # Load nodes links + target.node_tree.links.clear() + + load_links(data["node_tree"]["links"], target.node_tree) def _dump_implementation(self, data, instance=None): assert(instance) @@ -367,7 +259,15 @@ class BlMaterial(BlDatablock): ] data = mat_dumper.dump(instance) - if instance.is_grease_pencil: + if instance.use_nodes: + nodes = {} + data["node_tree"] = {} + for node in instance.node_tree.nodes: + nodes[node.name] = dump_node(node) + data["node_tree"]['nodes'] = nodes + + data["node_tree"]["links"] = dump_links(instance.node_tree.links) + elif instance.is_grease_pencil: gp_mat_dumper = Dumper() gp_mat_dumper.depth = 3 @@ -399,9 +299,6 @@ class BlMaterial(BlDatablock): # 'fill_image', ] data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil) - elif instance.use_nodes: - data['node_tree'] = dump_shader_node_tree(instance.node_tree) - return data def _resolve_deps_implementation(self): diff --git a/multi_user/bl_types/bl_mesh.py b/multi_user/bl_types/bl_mesh.py index 70546b7..7ee32d5 100644 --- a/multi_user/bl_types/bl_mesh.py +++ b/multi_user/bl_types/bl_mesh.py @@ -25,7 +25,7 @@ import numpy as np from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection from replication.constants import DIFF_BINARY from replication.exception import ContextError -from .bl_datablock import BlDatablock, get_datablock_from_uuid +from .bl_datablock import BlDatablock VERTICE = ['co'] @@ -70,17 +70,8 @@ class BlMesh(BlDatablock): # 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) + for m in data["material_list"]: + target.materials.append(bpy.data.materials[m]) # CLEAR GEOMETRY if target.vertices: @@ -172,7 +163,12 @@ class BlMesh(BlDatablock): 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] + m_list = [] + for material in instance.materials: + if material: + m_list.append(material.name) + + data['material_list'] = m_list return data diff --git a/multi_user/bl_types/bl_node_group.py b/multi_user/bl_types/bl_node_group.py deleted file mode 100644 index 8ebf568..0000000 --- a/multi_user/bl_types/bl_node_group.py +++ /dev/null @@ -1,47 +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 .dump_anything import Dumper, Loader, np_dump_collection, np_load_collection -from .bl_datablock import BlDatablock -from .bl_material import (dump_shader_node_tree, - load_shader_node_tree, - get_node_tree_dependencies) - -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' - - def _construct(self, data): - return bpy.data.node_groups.new(data["name"], data["type"]) - - def _load_implementation(self, data, target): - load_shader_node_tree(data, target) - - def _dump_implementation(self, data, instance=None): - return dump_shader_node_tree(instance) - - def _resolve_deps_implementation(self): - return get_node_tree_dependencies(self.instance) \ No newline at end of file diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 4d0bc79..021fb35 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -24,6 +24,7 @@ from replication.exception import ContextError from .bl_datablock import BlDatablock, get_datablock_from_uuid from .dump_anything import Dumper, Loader +from replication.exception import ReparentException def load_pose(target_bone, data): @@ -119,7 +120,9 @@ class BlObject(BlDatablock): data_uuid = data.get("data_uuid") data_id = data.get("data") - if target.data and (target.data.name != data_id): + if target.type != data['type']: + raise ReparentException() + elif target.data and (target.data.name != data_id): target.data = get_datablock_from_uuid(data_uuid, find_data_from_name(data_id), ignore=['images']) # vertex groups @@ -188,10 +191,10 @@ class BlObject(BlDatablock): target_bone.bone_group = target.pose.bone_group[bone_data['bone_group_index']] # TODO: find another way... - if target.empty_display_type == "IMAGE": + if target.type == 'EMPTY': img_uuid = data.get('data_uuid') if target.data is None and img_uuid: - target.data = get_datablock_from_uuid(img_uuid, None) + target.data = get_datablock_from_uuid(img_uuid, None)#bpy.data.images.get(img_key, None) def _dump_implementation(self, data, instance=None): assert(instance) diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index 02f0c89..5597493 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -16,18 +16,15 @@ # ##### END GPL LICENSE BLOCK ##### -import logging - import bpy import mathutils -from deepdiff import DeepDiff -from replication.constants import DIFF_JSON, MODIFIED -from .bl_collection import (dump_collection_children, dump_collection_objects, - load_collection_childrens, load_collection_objects, - resolve_collection_dependencies) +from .dump_anything import Loader, Dumper from .bl_datablock import BlDatablock -from .dump_anything import Dumper, Loader +from .bl_collection import dump_collection_children, dump_collection_objects, load_collection_childrens, load_collection_objects +from replication.constants import (DIFF_JSON, MODIFIED) +from deepdiff import DeepDiff +import logging RENDER_SETTINGS = [ 'dither_intensity', @@ -264,12 +261,6 @@ VIEW_SETTINGS = [ 'black_level' ] - - - - - - class BlScene(BlDatablock): bl_id = "scenes" bl_class = bpy.types.Scene @@ -319,7 +310,7 @@ class BlScene(BlDatablock): if 'view_settings' in data.keys(): loader.load(target.view_settings, data['view_settings']) if target.view_settings.use_curve_mapping and \ - 'curve_mapping' in data['view_settings']: + 'curve_mapping' in data['view_settings']: # TODO: change this ugly fix target.view_settings.curve_mapping.white_level = data[ 'view_settings']['curve_mapping']['white_level'] @@ -329,8 +320,8 @@ class BlScene(BlDatablock): def _dump_implementation(self, data, instance=None): assert(instance) + data = {} - # Metadata scene_dumper = Dumper() scene_dumper.depth = 1 scene_dumper.include_filter = [ @@ -345,9 +336,11 @@ class BlScene(BlDatablock): if self.preferences.sync_flags.sync_active_camera: scene_dumper.include_filter.append('camera') - data.update(scene_dumper.dump(instance)) + data = scene_dumper.dump(instance) - # Master collection + scene_dumper.depth = 3 + + scene_dumper.include_filter = ['children', 'objects', 'name'] data['collection'] = {} data['collection']['children'] = dump_collection_children( instance.collection) @@ -357,7 +350,6 @@ class BlScene(BlDatablock): scene_dumper.depth = 1 scene_dumper.include_filter = None - # Render settings if self.preferences.sync_flags.sync_render_settings: scene_dumper.include_filter = RENDER_SETTINGS @@ -385,18 +377,18 @@ 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 - return data def _resolve_deps_implementation(self): deps = [] - # Master Collection - deps.extend(resolve_collection_dependencies(self.instance.collection)) + # child collections + for child in self.instance.collection.children: + deps.append(child) + + # childs objects + for object in self.instance.collection.objects: + deps.append(object) # world if self.instance.world: @@ -406,11 +398,6 @@ class BlScene(BlDatablock): if self.instance.grease_pencil: 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) - return deps def diff(self): diff --git a/multi_user/bl_types/bl_sequencer.py b/multi_user/bl_types/bl_sequencer.py deleted file mode 100644 index b2376fd..0000000 --- a/multi_user/bl_types/bl_sequencer.py +++ /dev/null @@ -1,197 +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' - - 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): - scene = bpy.data.scenes.get(self.data['name'], None) - if scene: - if scene.sequence_editor is None: - 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/bl_world.py b/multi_user/bl_types/bl_world.py index 99ba1ae..f641c9f 100644 --- a/multi_user/bl_types/bl_world.py +++ b/multi_user/bl_types/bl_world.py @@ -21,8 +21,10 @@ import mathutils from .dump_anything import Loader, Dumper from .bl_datablock import BlDatablock -from .bl_material import (load_shader_node_tree, - dump_shader_node_tree, +from .bl_material import (load_links, + load_node, + dump_node, + dump_links, get_node_tree_dependencies) @@ -46,7 +48,15 @@ class BlWorld(BlDatablock): if target.node_tree is None: target.use_nodes = True - load_shader_node_tree(data['node_tree'], target.node_tree) + target.node_tree.nodes.clear() + + for node in data["node_tree"]["nodes"]: + load_node(data["node_tree"]["nodes"][node], target.node_tree) + + # Load nodes links + target.node_tree.links.clear() + + load_links(data["node_tree"]["links"], target.node_tree) def _dump_implementation(self, data, instance=None): assert(instance) @@ -60,7 +70,15 @@ class BlWorld(BlDatablock): ] data = world_dumper.dump(instance) if instance.use_nodes: - data['node_tree'] = dump_shader_node_tree(instance.node_tree) + data['node_tree'] = {} + nodes = {} + + for node in instance.node_tree.nodes: + nodes[node.name] = dump_node(node) + + data["node_tree"]['nodes'] = nodes + + data["node_tree"]['links'] = dump_links(instance.node_tree.links) return data diff --git a/multi_user/delayable.py b/multi_user/delayable.py index 70ecfe6..5fefee8 100644 --- a/multi_user/delayable.py +++ b/multi_user/delayable.py @@ -36,7 +36,8 @@ from replication.constants import (FETCHED, STATE_ACTIVE, STATE_SYNCING, STATE_LOBBY, - STATE_SRV_SYNC) + STATE_SRV_SYNC, + REPARENT) from replication.interface import session from replication.exception import NonAuthorizedOperationError @@ -121,6 +122,15 @@ class ApplyTimer(Timer): session.apply(node) except Exception as e: logging.error(f"Fail to apply {node_ref.uuid}: {e}") + elif node_ref.state == REPARENT: + # Reload the node + node_ref.remove_instance() + node_ref.resolve() + session.apply(node) + for parent in session._graph.find_parents(node): + logging.info(f"Applying parent {parent}") + session.apply(parent, force=True) + node_ref.state = UP class DynamicRightSelectTimer(Timer): @@ -161,8 +171,7 @@ class DynamicRightSelectTimer(Timer): session.change_owner( node.uuid, RP_COMMON, - ignore_warnings=True, - affect_dependencies=recursive) + recursive=recursive) except NonAuthorizedOperationError: logging.warning(f"Not authorized to change {node} owner") @@ -179,8 +188,7 @@ class DynamicRightSelectTimer(Timer): session.change_owner( node.uuid, settings.username, - ignore_warnings=True, - affect_dependencies=recursive) + recursive=recursive) except NonAuthorizedOperationError: logging.warning(f"Not authorized to change {node} owner") else: @@ -205,8 +213,7 @@ class DynamicRightSelectTimer(Timer): session.change_owner( key, RP_COMMON, - ignore_warnings=True, - affect_dependencies=recursive) + recursive=recursive) except NonAuthorizedOperationError: logging.warning(f"Not authorized to change {key} owner") diff --git a/multi_user/environment.py b/multi_user/environment.py index 5fc47a3..8796a4c 100644 --- a/multi_user/environment.py +++ b/multi_user/environment.py @@ -62,9 +62,6 @@ def install_package(name, version): del env["PIP_REQUIRE_VIRTUALENV"] subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", f"{name}=={version}"], env=env) - if name in sys.modules: - del sys.modules[name] - def check_package_version(name, required_version): logging.info(f"Checking {name} version...") out = subprocess.run([str(PYTHON_PATH), "-m", "pip", "show", name], capture_output=True) diff --git a/multi_user/operators.py b/multi_user/operators.py index dc8611c..4a7adbf 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -166,8 +166,7 @@ class SessionStartOperator(bpy.types.Operator): # init the factory with supported types for type in bl_types.types_to_register(): type_module = getattr(bl_types, type) - name = [e.capitalize() for e in type.split('_')[1:]] - type_impl_name = 'Bl'+''.join(name) + type_impl_name = f"Bl{type.split('_')[1].capitalize()}" type_module_class = getattr(type_module, type_impl_name) supported_bl_types.append(type_module_class.bl_id) @@ -227,8 +226,7 @@ class SessionStartOperator(bpy.types.Operator): except Exception as e: self.report({'ERROR'}, repr(e)) logging.error(f"Error: {e}") - import traceback - traceback.print_exc() + # Join a session else: if not runtime_settings.admin: @@ -428,8 +426,7 @@ class SessionPropertyRightOperator(bpy.types.Operator): if session: session.change_owner(self.key, runtime_settings.clients, - ignore_warnings=True, - affect_dependencies=self.recursive) + recursive=self.recursive) return {"FINISHED"} diff --git a/multi_user/preferences.py b/multi_user/preferences.py index 0548da7..d728c9c 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -29,9 +29,8 @@ from .utils import get_preferences, get_expanded_icon from replication.constants import RP_COMMON from replication.interface import session -# From https://stackoverflow.com/a/106223 -IP_REGEX = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") -HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$") +IP_EXPR = re.compile('\d+\.\d+\.\d+\.\d+') + def randomColor(): """Generate a random color """ @@ -54,13 +53,10 @@ def update_panel_category(self, context): def update_ip(self, context): - ip = IP_REGEX.search(self.ip) - dns = HOSTNAME_REGEX.search(self.ip) + ip = IP_EXPR.search(self.ip) if ip: self['ip'] = ip.group() - elif dns: - self['ip'] = dns.group() else: logging.error("Wrong IP format") self['ip'] = "127.0.0.1" @@ -242,31 +238,6 @@ class SessionPrefs(bpy.types.AddonPreferences): set=set_log_level, get=get_log_level ) - presence_hud_scale: bpy.props.FloatProperty( - name="Text scale", - description="Adjust the session widget text scale", - min=7, - max=90, - default=25, - ) - presence_hud_hpos: bpy.props.FloatProperty( - name="Horizontal position", - description="Adjust the session widget horizontal position", - min=1, - max=90, - default=1, - step=1, - subtype='PERCENTAGE', - ) - presence_hud_vpos: bpy.props.FloatProperty( - name="Vertical position", - description="Adjust the session widget vertical position", - min=1, - max=94, - default=1, - step=1, - subtype='PERCENTAGE', - ) conf_session_identity_expanded: bpy.props.BoolProperty( name="Identity", description="Identity", @@ -441,15 +412,6 @@ class SessionPrefs(bpy.types.AddonPreferences): emboss=False) if self.conf_session_ui_expanded: box.row().prop(self, "panel_category", text="Panel category", expand=True) - row = box.row() - row.label(text="Session widget:") - - col = box.column(align=True) - col.prop(self, "presence_hud_scale", expand=True) - - - col.prop(self, "presence_hud_hpos", expand=True) - col.prop(self, "presence_hud_vpos", expand=True) if self.category == 'UPDATE': from . import addon_updater_ops @@ -462,9 +424,9 @@ class SessionPrefs(bpy.types.AddonPreferences): new_db = self.supported_datablocks.add() type_module = getattr(bl_types, type) - name = [e.capitalize() for e in type.split('_')[1:]] - type_impl_name = 'Bl'+''.join(name) + type_impl_name = f"Bl{type.split('_')[1].capitalize()}" 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 diff --git a/multi_user/presence.py b/multi_user/presence.py index 4776e03..b885013 100644 --- a/multi_user/presence.py +++ b/multi_user/presence.py @@ -35,7 +35,7 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG, STATE_SYNCING, STATE_WAITING) from replication.interface import session -from .utils import find_from_attr, get_state_str, get_preferences +from .utils import find_from_attr, get_state_str # Helper functions @@ -300,38 +300,41 @@ class UserSelectionWidget(Widget): ob = find_from_attr("uuid", select_ob, bpy.data.objects) if not ob: return + + position = None - vertex_pos = bbox_from_obj(ob, 1.0) - vertex_indices = ((0, 1), (0, 2), (1, 3), (2, 3), - (4, 5), (4, 6), (5, 7), (6, 7), - (0, 4), (1, 5), (2, 6), (3, 7)) - - if ob.instance_collection: - for obj in ob.instance_collection.objects: - if obj.type == 'MESH' and hasattr(obj, 'bound_box'): - vertex_pos = get_bb_coords_from_obj(obj, instance=ob) - break - elif ob.type == 'EMPTY': - vertex_pos = bbox_from_obj(ob, ob.empty_display_size) - elif ob.type == 'LIGHT': - vertex_pos = bbox_from_obj(ob, ob.data.shadow_soft_size) - elif ob.type == 'LIGHT_PROBE': - vertex_pos = bbox_from_obj(ob, ob.data.influence_distance) - elif ob.type == 'CAMERA': - vertex_pos = bbox_from_obj(ob, ob.data.display_size) - elif hasattr(ob, 'bound_box'): - vertex_indices = ( + if ob.type == 'EMPTY': + # TODO: Child case + # Collection instance case + indices = ( (0, 1), (1, 2), (2, 3), (0, 3), (4, 5), (5, 6), (6, 7), (4, 7), (0, 4), (1, 5), (2, 6), (3, 7)) - vertex_pos = get_bb_coords_from_obj(ob) + if ob.instance_collection: + for obj in ob.instance_collection.objects: + if obj.type == 'MESH' and hasattr(obj, 'bound_box'): + positions = get_bb_coords_from_obj(obj, instance=ob) + break + elif hasattr(ob, 'bound_box'): + indices = ( + (0, 1), (1, 2), (2, 3), (0, 3), + (4, 5), (5, 6), (6, 7), (4, 7), + (0, 4), (1, 5), (2, 6), (3, 7)) + positions = get_bb_coords_from_obj(ob) + if positions is None: + indices = ( + (0, 1), (0, 2), (1, 3), (2, 3), + (4, 5), (4, 6), (5, 7), (6, 7), + (0, 4), (1, 5), (2, 6), (3, 7)) + + positions = bbox_from_obj(ob, ob.scale.x) shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR') batch = batch_for_shader( shader, 'LINES', - {"pos": vertex_pos}, - indices=vertex_indices) + {"pos": positions}, + indices=indices) shader.bind() shader.uniform_float("color", self.data.get('color')) @@ -384,9 +387,6 @@ class UserNameWidget(Widget): class SessionStatusWidget(Widget): draw_type = 'POST_PIXEL' - def __init__(self): - self.preferences = get_preferences() - @property def settings(self): return getattr(bpy.context.window_manager, 'session', None) @@ -396,8 +396,6 @@ class SessionStatusWidget(Widget): self.settings.enable_presence def draw(self): - text_scale = self.preferences.presence_hud_scale - ui_scale = bpy.context.preferences.view.ui_scale color = [1, 1, 0, 1] state = session.state.get('STATE') state_str = f"{get_state_str(state)}" @@ -406,11 +404,9 @@ class SessionStatusWidget(Widget): color = [0, 1, 0, 1] elif state == STATE_INITIAL: color = [1, 0, 0, 1] - hpos = (self.preferences.presence_hud_hpos*bpy.context.area.width)/100 - vpos = (self.preferences.presence_hud_vpos*bpy.context.area.height)/100 - blf.position(0, hpos, vpos, 0) - blf.size(0, int(text_scale*ui_scale), 72) + blf.position(0, 10, 20, 0) + blf.size(0, 16, 45) blf.color(0, color[0], color[1], color[2], color[3]) blf.draw(0, state_str) diff --git a/multi_user/ui.py b/multi_user/ui.py index bf3fbb3..1f90ec8 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -448,17 +448,9 @@ class SESSION_PT_presence(bpy.types.Panel): layout = self.layout settings = context.window_manager.session - pref = get_preferences() layout.active = settings.enable_presence col = layout.column() col.prop(settings, "presence_show_session_status") - row = col.column() - row.active = settings.presence_show_session_status - row.prop(pref, "presence_hud_scale", expand=True) - row = col.column(align=True) - row.active = settings.presence_show_session_status - row.prop(pref, "presence_hud_hpos", expand=True) - row.prop(pref, "presence_hud_vpos", expand=True) col.prop(settings, "presence_show_selected") col.prop(settings, "presence_show_user") row = layout.column() @@ -630,7 +622,7 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel): col.prop(settings, "presence_show_session_status") col.prop(settings, "presence_show_selected") col.prop(settings, "presence_show_user") - + row = layout.column() row.active = settings.presence_show_user row.prop(settings, "presence_show_far_user") diff --git a/multi_user/utils.py b/multi_user/utils.py index 57ed532..a8317c3 100644 --- a/multi_user/utils.py +++ b/multi_user/utils.py @@ -99,9 +99,7 @@ def clean_scene(): type_collection.remove(item) except: continue - - # Clear sequencer - bpy.context.scene.sequence_editor_clear() + def get_selected_objects(scene, active_view_layer): return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]