From 40ad96b0af4578b3a74c900bdd26238a048079b9 Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Wed, 11 Mar 2020 17:45:56 +0100 Subject: [PATCH 01/66] feat: initial particle system support Related to #24 --- multi_user/bl_types/__init__.py | 3 ++- multi_user/bl_types/bl_datablock.py | 4 ++-- multi_user/bl_types/bl_object.py | 26 ++++++++++++++++++++--- multi_user/bl_types/bl_particle.py | 32 +++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 multi_user/bl_types/bl_particle.py diff --git a/multi_user/bl_types/__init__.py b/multi_user/bl_types/__init__.py index c3e9605..81f744a 100644 --- a/multi_user/bl_types/__init__.py +++ b/multi_user/bl_types/__init__.py @@ -16,7 +16,8 @@ __all__ = [ 'bl_metaball', 'bl_lattice', 'bl_lightprobe', - 'bl_speaker' + 'bl_speaker', + 'bl_particle' ] # Order here defines execution order from . import * diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index be6c8ed..c18dfcd 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -3,7 +3,7 @@ import mathutils from .. import utils from ..libs.replication.replication.data import ReplicatedDatablock -from ..libs.replication.replication.constants import (UP, DIFF_BINARY) +from ..libs.replication.replication.constants import (UP, DIFF_BINARY,DIFF_JSON) from ..libs import dump_anything def dump_driver(driver): @@ -75,7 +75,7 @@ class BlDatablock(ReplicatedDatablock): if self.pointer and hasattr(self.pointer, 'uuid'): self.pointer.uuid = self.uuid - self.diff_method = DIFF_BINARY + self.diff_method = DIFF_JSON def library_apply(self): """Apply stored data diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 26b5469..f9ef377 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -101,14 +101,19 @@ class BlObject(BlDatablock): for modifier in data['modifiers']: target_modifier = target.modifiers.get(modifier) - if not target_modifier: target_modifier = target.modifiers.new( data['modifiers'][modifier]['name'], data['modifiers'][modifier]['type']) + if target_modifier.type == 'PARTICLE_SYSTEM': + tmp_particle_system = target_modifier.particle_system.name + utils.dump_anything.load( target_modifier, data['modifiers'][modifier]) + if target_modifier.type == 'PARTICLE_SYSTEM': + target.particle_systems[data['modifiers'][modifier]['name']].settings = bpy.data.particles[data['modifiers'][modifier]['particle_system']] + # bpy.data.particles.remove(tmp_particle_system) # Load constraints # Object if hasattr(target, 'constraints') and 'constraints' in data: @@ -188,6 +193,7 @@ class BlObject(BlDatablock): target.data.shape_keys.key_blocks[key_block].relative_key = target.data.shape_keys.key_blocks[reference] + def dump_implementation(self, data, pointer=None): assert(pointer) dumper = utils.dump_anything.Dumper() @@ -219,9 +225,17 @@ class BlObject(BlDatablock): dumper.depth = 2 data["modifiers"] = {} for index, modifier in enumerate(pointer.modifiers): - data["modifiers"][modifier.name] = dumper.dump(modifier) - data["modifiers"][modifier.name]['m_index'] = index + modifier_data = {} + if modifier.type == 'PARTICLE_SYSTEM': + modifier_data['particle_system'] = modifier.particle_system.name + dumper.depth = 1 + + modifier_data.update(dumper.dump(modifier)) + + modifier_data['m_index'] = index + + data["modifiers"][modifier.name] = modifier_data # CONSTRAINTS # OBJECT if hasattr(pointer, 'constraints'): @@ -334,9 +348,15 @@ class BlObject(BlDatablock): # Avoid Empty case if self.pointer.data: deps.append(self.pointer.data) + + # Childred if len(self.pointer.children) > 0: deps.extend(list(self.pointer.children)) + # Particle systems + for particle_slot in self.pointer.particle_systems: + deps.append(bpy.data.particles[particle_slot.name]) + if self.is_library: deps.append(self.pointer.library) diff --git a/multi_user/bl_types/bl_particle.py b/multi_user/bl_types/bl_particle.py new file mode 100644 index 0000000..4d08f33 --- /dev/null +++ b/multi_user/bl_types/bl_particle.py @@ -0,0 +1,32 @@ +import bpy +import mathutils + +from .. import utils +from ..libs.replication.replication.constants import (DIFF_JSON) +from .bl_datablock import BlDatablock + + +class BlParticle(BlDatablock): + bl_id = "particles" + bl_class = bpy.types.ParticleSettings + bl_delay_refresh = 1 + bl_delay_apply = 1 + bl_automatic_push = True + bl_icon = 'PARTICLES' + + diff_method = DIFF_JSON + + def construct(self, data): + return bpy.data.particles.new(data["name"]) + + def load_implementation(self, data, target): + utils.dump_anything.load(target, data) + + def dump_implementation(self, data, pointer=None): + assert(pointer) + + dumper = utils.dump_anything.Dumper() + dumper.depth = 1 + data = dumper.dump(pointer) + + return data From 5f95eadc1dad680d1bdc8d0228f071d846a1a8d3 Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Wed, 11 Mar 2020 18:37:43 +0100 Subject: [PATCH 02/66] feat: test particle cache access --- multi_user/bl_types/bl_object.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index f9ef377..b7f356f 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -340,6 +340,9 @@ class BlObject(BlDatablock): key_blocks[key.name]['relative_key'] = key.relative_key.name data['shape_keys']['key_blocks'] = key_blocks + if pointer.particle_systems: + psys = pointer.evaluated_get(bpy.context.evaluated_depsgraph_get()).particle_systems + print(len(psys[0].particles)) return data def resolve_deps_implementation(self): From 505f3ab770aced00999edba1462cced4a85bc59f Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 23 Feb 2021 13:20:01 +0100 Subject: [PATCH 03/66] fix: external depencies removed during undo --- multi_user/bl_types/bl_file.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/multi_user/bl_types/bl_file.py b/multi_user/bl_types/bl_file.py index 5801306..120048d 100644 --- a/multi_user/bl_types/bl_file.py +++ b/multi_user/bl_types/bl_file.py @@ -68,12 +68,15 @@ class BlFile(ReplicatedDatablock): self.preferences = utils.get_preferences() def resolve(self, construct = True): - if self.data: - self.instance = Path(get_filepath(self.data['name'])) + self.instance = Path(get_filepath(self.data['name'])) + + file_exists = self.instance.exists() + if not file_exists: + logging.debug("File don't exist, loading it.") + self._load(self.data, self.instance) + + return file_exists - if not self.instance.exists(): - logging.debug("File don't exist, loading it.") - self._load(self.data, self.instance) def push(self, socket, identity=None, check_data=False): super().push(socket, identity=None, check_data=False) From 317fc03f87f84a963ac234019c95894c6fa3f6f2 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 26 Feb 2021 10:38:50 +0100 Subject: [PATCH 04/66] feat: geometry node initial support refactor: resolve datablocks by uuid by devault fix: modifier texture dependencies --- multi_user/bl_types/bl_datablock.py | 12 +++++------- multi_user/bl_types/bl_material.py | 17 +++++++++-------- multi_user/bl_types/bl_node_group.py | 10 +++++----- multi_user/bl_types/bl_object.py | 20 ++++++++++++++------ multi_user/bl_types/bl_world.py | 8 ++++---- multi_user/bl_types/dump_anything.py | 2 ++ 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index b7cc450..c3ab5a7 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -127,16 +127,14 @@ class BlDatablock(ReplicatedDatablock): instance.uuid = self.uuid def resolve(self, construct = True): - datablock_ref = None datablock_root = getattr(bpy.data, self.bl_id) - - try: - datablock_ref = datablock_root[self.data['name']] - except Exception: - pass + datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root) if not datablock_ref: - datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root) + try: + datablock_ref = datablock_root[self.data['name']] + except Exception: + pass if construct and not datablock_ref: name = self.data.get('name') diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 8e62ed2..e815831 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -27,7 +27,7 @@ from .dump_anything import Loader, Dumper from .bl_datablock import BlDatablock, get_datablock_from_uuid NODE_SOCKET_INDEX = re.compile('\[(\d*)\]') - +IGNORED_SOCKETS = ['GEOMETRY'] def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): """ Load a node into a node_tree from a dict @@ -52,7 +52,8 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): inputs_data = node_data.get('inputs') if inputs_data: - inputs = target_node.inputs + inputs = [i for i in target_node.inputs if i.type not in IGNORED_SOCKETS] + for idx, inpt in enumerate(inputs_data): if idx < len(inputs) and hasattr(inputs[idx], "default_value"): try: @@ -64,12 +65,12 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): outputs_data = node_data.get('outputs') if outputs_data: - outputs = target_node.outputs + outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS] for idx, output in enumerate(outputs_data): if idx < len(outputs) and hasattr(outputs[idx], "default_value"): try: outputs[idx].default_value = output - except: + except Exception as e: logging.warning( f"Node {target_node.name} output {outputs[idx].name} parameter not supported, skipping ({e})") else: @@ -206,7 +207,7 @@ def dump_node(node: bpy.types.ShaderNode) -> dict: return dumped_node -def dump_shader_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict: +def dump_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 @@ -276,7 +277,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_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 @@ -387,7 +388,7 @@ class BlMaterial(BlDatablock): if target.node_tree is None: target.use_nodes = True - load_shader_node_tree(data['node_tree'], target.node_tree) + load_node_tree(data['node_tree'], target.node_tree) def _dump_implementation(self, data, instance=None): assert(instance) @@ -454,7 +455,7 @@ class BlMaterial(BlDatablock): ] data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil) elif instance.use_nodes: - data['node_tree'] = dump_shader_node_tree(instance.node_tree) + data['node_tree'] = dump_node_tree(instance.node_tree) return data diff --git a/multi_user/bl_types/bl_node_group.py b/multi_user/bl_types/bl_node_group.py index a353659..1e0d249 100644 --- a/multi_user/bl_types/bl_node_group.py +++ b/multi_user/bl_types/bl_node_group.py @@ -21,13 +21,13 @@ 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, +from .bl_material import (dump_node_tree, + load_node_tree, get_node_tree_dependencies) class BlNodeGroup(BlDatablock): bl_id = "node_groups" - bl_class = bpy.types.ShaderNodeTree + bl_class = bpy.types.NodeTree bl_check_common = False bl_icon = 'NODETREE' bl_reload_parent = False @@ -36,10 +36,10 @@ class BlNodeGroup(BlDatablock): return bpy.data.node_groups.new(data["name"], data["type"]) def _load_implementation(self, data, target): - load_shader_node_tree(data, target) + load_node_tree(data, target) def _dump_implementation(self, data, instance=None): - return dump_shader_node_tree(instance) + return dump_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 279c772..51343ac 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -91,18 +91,25 @@ def _is_editmode(object: bpy.types.Object) -> bool: child_data.is_editmode) -def find_textures_dependencies(collection): +def find_textures_dependencies(modifiers): """ Check collection """ textures = [] - for item in collection: - for attr in dir(item): - inst = getattr(item, attr) - if issubclass(type(inst), bpy.types.Texture) and inst is not None: - textures.append(inst) + for mod in modifiers: + modifier_attributes = [getattr(mod, attr_name) for attr_name in mod.bl_rna.properties.keys()] + for attr in modifier_attributes: + if issubclass(type(attr), bpy.types.Texture) and attr is not None: + textures.append(attr) return textures +def find_geometry_nodes_groups(collection): + nodes_groups = [] + for item in collection: + if item.type == 'NODES' and item.node_group: + nodes_groups.append(item.node_group) + + return nodes_groups def dump_vertex_groups(src_object: bpy.types.Object) -> dict: """ Dump object's vertex groups @@ -481,5 +488,6 @@ class BlObject(BlDatablock): if self.instance.modifiers: deps.extend(find_textures_dependencies(self.instance.modifiers)) + deps.extend(find_geometry_nodes_groups(self.instance.modifiers)) return deps diff --git a/multi_user/bl_types/bl_world.py b/multi_user/bl_types/bl_world.py index ee0f15d..a7c34c0 100644 --- a/multi_user/bl_types/bl_world.py +++ b/multi_user/bl_types/bl_world.py @@ -21,8 +21,8 @@ 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_node_tree, + dump_node_tree, get_node_tree_dependencies) @@ -44,7 +44,7 @@ class BlWorld(BlDatablock): if target.node_tree is None: target.use_nodes = True - load_shader_node_tree(data['node_tree'], target.node_tree) + load_node_tree(data['node_tree'], target.node_tree) def _dump_implementation(self, data, instance=None): assert(instance) @@ -58,7 +58,7 @@ 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'] = dump_node_tree(instance.node_tree) return data diff --git a/multi_user/bl_types/dump_anything.py b/multi_user/bl_types/dump_anything.py index 30c2a13..4765fbf 100644 --- a/multi_user/bl_types/dump_anything.py +++ b/multi_user/bl_types/dump_anything.py @@ -596,6 +596,8 @@ class Loader: instance.write(bpy.data.textures.get(dump)) elif isinstance(rna_property_type, T.ColorRamp): self._load_default(instance, dump) + elif isinstance(rna_property_type, T.NodeTree): + instance.write(bpy.data.node_groups.get(dump)) elif isinstance(rna_property_type, T.Object): instance.write(bpy.data.objects.get(dump)) elif isinstance(rna_property_type, T.Mesh): From c1c39438e3341cb9603455e9fb6c675c1a91001c Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 26 Feb 2021 12:27:56 +0100 Subject: [PATCH 05/66] feat: input value support --- multi_user/bl_types/bl_object.py | 50 ++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 51343ac..f5bf65e 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -17,7 +17,7 @@ import logging - +import re import bpy import mathutils from replication.exception import ContextError @@ -30,12 +30,50 @@ from .dump_anything import ( np_dump_collection) + SKIN_DATA = [ 'radius', 'use_loose', 'use_root' ] +def get_number(e): + return int(re.findall('[0-9]+', e)[0]) + +def dump_modifier_geometry_node_inputs(modifier): + inputs_name = [p for p in dir(modifier) if "Input_" in p] + inputs_name.sort(key=get_number) + dumped_inputs = [] + for inputs_index, input_name in enumerate(inputs_name): + input_value = modifier[input_name] + dumped_input = None + if isinstance(input_value, bpy.types.ID): + dumped_input = input_value.uuid + elif type(input_value) in [int, str, float]: + dumped_input = input_value + elif hasattr(input_value, 'to_list'): + dumped_input = input_value.to_list() + dumped_inputs.append(dumped_input) + + return dumped_inputs + +def load_modifier_geometry_node_inputs(dumped_modifier, target_modifier): + inputs_name = [p for p in dir(target_modifier) if "Input_" in p] + inputs_name.sort(key=get_number) + logging.info(inputs_name) + for input_index, input_name in enumerate(inputs_name): + dumped_value = dumped_modifier['inputs'][input_index] + input_value = target_modifier[input_name] + logging.info(input_name) + if type(input_value) in [int, str, float]: + input_value = dumped_value + elif hasattr(input_value, 'to_list'): + for index in range(len(input_value)): + input_value[index] = dumped_value[index] + else: + target_modifier[input_name] = get_datablock_from_uuid(dumped_value, None) + + def load_pose(target_bone, data): target_bone.rotation_mode = data['rotation_mode'] loader = Loader() @@ -282,6 +320,11 @@ class BlObject(BlDatablock): and 'cycles_visibility' in data: loader.load(target.cycles_visibility, data['cycles_visibility']) + if hasattr(target, 'modifiers'): + nodes_modifiers = [mod for mod in target.modifiers if mod.type == 'NODES'] + for modifier in nodes_modifiers: + load_modifier_geometry_node_inputs(data['modifiers'][modifier.name], modifier) + def _dump_implementation(self, data, instance=None): assert(instance) @@ -350,7 +393,10 @@ class BlObject(BlDatablock): dumper.depth = 1 for index, modifier in enumerate(modifiers): data["modifiers"][modifier.name] = dumper.dump(modifier) - + # hack to dump geometry nodes inputs + if modifier.type == 'NODES': + dumped_inputs = dump_modifier_geometry_node_inputs(modifier) + data["modifiers"][modifier.name]['inputs'] = dumped_inputs gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None) if gp_modifiers: From fb6f170d603c6210835e6758cea758999d77cbad Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 26 Feb 2021 14:31:05 +0100 Subject: [PATCH 06/66] clean: remove prints doc: add new def docstrings --- multi_user/bl_types/bl_object.py | 59 +++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index f5bf65e..810eb69 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -37,19 +37,25 @@ SKIN_DATA = [ 'use_root' ] -def get_number(e): +def get_input_index(e): return int(re.findall('[0-9]+', e)[0]) -def dump_modifier_geometry_node_inputs(modifier): + +def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list: + """ Dump geometry node modifier input properties + + :arg modifier: geometry node modifier to dump + :type modifier: bpy.type.Modifier + """ inputs_name = [p for p in dir(modifier) if "Input_" in p] - inputs_name.sort(key=get_number) + inputs_name.sort(key=get_input_index) dumped_inputs = [] for inputs_index, input_name in enumerate(inputs_name): input_value = modifier[input_name] dumped_input = None if isinstance(input_value, bpy.types.ID): dumped_input = input_value.uuid - elif type(input_value) in [int, str, float]: + elif type(input_value) in [int, str, float]: dumped_input = input_value elif hasattr(input_value, 'to_list'): dumped_input = input_value.to_list() @@ -57,21 +63,29 @@ def dump_modifier_geometry_node_inputs(modifier): return dumped_inputs -def load_modifier_geometry_node_inputs(dumped_modifier, target_modifier): + +def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: bpy.types.Modifier): + """ Load geometry node modifier inputs + + :arg dumped_modifier: source dumped modifier to load + :type dumped_modifier: dict + :arg target_modifier: target geometry node modifier + :type target_modifier: bpy.type.Modifier + """ + inputs_name = [p for p in dir(target_modifier) if "Input_" in p] - inputs_name.sort(key=get_number) - logging.info(inputs_name) + inputs_name.sort(key=get_input_index) for input_index, input_name in enumerate(inputs_name): dumped_value = dumped_modifier['inputs'][input_index] input_value = target_modifier[input_name] - logging.info(input_name) - if type(input_value) in [int, str, float]: + if type(input_value) in [int, str, float]: input_value = dumped_value elif hasattr(input_value, 'to_list'): for index in range(len(input_value)): input_value[index] = dumped_value[index] else: - target_modifier[input_name] = get_datablock_from_uuid(dumped_value, None) + target_modifier[input_name] = get_datablock_from_uuid( + dumped_value, None) def load_pose(target_bone, data): @@ -129,21 +143,33 @@ def _is_editmode(object: bpy.types.Object) -> bool: child_data.is_editmode) -def find_textures_dependencies(modifiers): - """ Check collection +def find_textures_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.Texture]: + """ Find textures lying in a modifier stack + + :arg modifiers: modifiers collection + :type modifiers: bpy.types.bpy_prop_collection + :return: list of bpy.types.Texture pointers """ textures = [] for mod in modifiers: - modifier_attributes = [getattr(mod, attr_name) for attr_name in mod.bl_rna.properties.keys()] + modifier_attributes = [getattr(mod, attr_name) + for attr_name in mod.bl_rna.properties.keys()] for attr in modifier_attributes: if issubclass(type(attr), bpy.types.Texture) and attr is not None: textures.append(attr) return textures -def find_geometry_nodes_groups(collection): + +def find_geometry_nodes(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]: + """ Find geometry nodes group from a modifier stack + + :arg modifiers: modifiers collection + :type modifiers: bpy.types.bpy_prop_collection + :return: list of bpy.types.NodeTree pointers + """ nodes_groups = [] - for item in collection: + for item in modifiers: if item.type == 'NODES' and item.node_group: nodes_groups.append(item.node_group) @@ -320,6 +346,7 @@ class BlObject(BlDatablock): and 'cycles_visibility' in data: loader.load(target.cycles_visibility, data['cycles_visibility']) + # TODO: handle geometry nodes input from dump_anything if hasattr(target, 'modifiers'): nodes_modifiers = [mod for mod in target.modifiers if mod.type == 'NODES'] for modifier in nodes_modifiers: @@ -534,6 +561,6 @@ class BlObject(BlDatablock): if self.instance.modifiers: deps.extend(find_textures_dependencies(self.instance.modifiers)) - deps.extend(find_geometry_nodes_groups(self.instance.modifiers)) + deps.extend(find_geometry_nodes(self.instance.modifiers)) return deps From 7049c1723de69c33da8a16bf7fe94fc5149cd4a0 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 2 Mar 2021 09:58:06 +0100 Subject: [PATCH 07/66] feat: initial CI job for building the documentation for gitlab page --- .gitlab-ci.yml | 3 +++ .gitlab/ci/doc.gitlab-ci.yml | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 .gitlab/ci/doc.gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36eedb7..36d3def 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,10 +1,13 @@ stages: - test - build + - doc - deploy + include: - local: .gitlab/ci/test.gitlab-ci.yml - local: .gitlab/ci/build.gitlab-ci.yml - local: .gitlab/ci/deploy.gitlab-ci.yml + - local: .gitlab/ci/doc.gitlab-ci.yml diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml new file mode 100644 index 0000000..fb08c61 --- /dev/null +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -0,0 +1,11 @@ +pages: + stage: doc + image: python:3.7-alpine + script: + - pip install -U sphinx sphinx_rtd_theme + - sphinx-build -b html ./docs public + artifacts: + paths: + - public + + From 55ca8a7b84dd920575206c6269feceb5fdfbc30a Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Tue, 2 Mar 2021 09:03:19 +0000 Subject: [PATCH 08/66] Update .gitlab/ci/doc.gitlab-ci.yml --- .gitlab/ci/doc.gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index fb08c61..7411d45 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -1,4 +1,5 @@ pages: + needs: ["build"] stage: doc image: python:3.7-alpine script: From 238a34d0236f6a6daa03677479dc2646f593af86 Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Tue, 2 Mar 2021 09:05:21 +0000 Subject: [PATCH 09/66] feat: needs test to success --- .gitlab/ci/build.gitlab-ci.yml | 1 + .gitlab/ci/deploy.gitlab-ci.yml | 1 + .gitlab/ci/doc.gitlab-ci.yml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index 9b75485..13cac6d 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -1,5 +1,6 @@ build: stage: build + needs: ["test"] image: debian:stable-slim script: - rm -rf tests .git .gitignore script diff --git a/.gitlab/ci/deploy.gitlab-ci.yml b/.gitlab/ci/deploy.gitlab-ci.yml index cb63ba9..298dabe 100644 --- a/.gitlab/ci/deploy.gitlab-ci.yml +++ b/.gitlab/ci/deploy.gitlab-ci.yml @@ -1,5 +1,6 @@ deploy: stage: deploy + needs: ["test"] image: slumber/docker-python variables: DOCKER_DRIVER: overlay2 diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 7411d45..2ea7123 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -1,5 +1,5 @@ pages: - needs: ["build"] + needs: ["test"] stage: doc image: python:3.7-alpine script: From d2215b662c72c219201786e49b59a398b5c771cd Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Tue, 2 Mar 2021 09:06:32 +0000 Subject: [PATCH 10/66] feat: update jobs dependencies --- .gitlab/ci/deploy.gitlab-ci.yml | 2 +- .gitlab/ci/doc.gitlab-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab/ci/deploy.gitlab-ci.yml b/.gitlab/ci/deploy.gitlab-ci.yml index 298dabe..1fe0489 100644 --- a/.gitlab/ci/deploy.gitlab-ci.yml +++ b/.gitlab/ci/deploy.gitlab-ci.yml @@ -1,6 +1,6 @@ deploy: stage: deploy - needs: ["test"] + needs: ["build"] image: slumber/docker-python variables: DOCKER_DRIVER: overlay2 diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 2ea7123..0f21148 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -1,5 +1,5 @@ pages: - needs: ["test"] + needs: ["build","deploy"] stage: doc image: python:3.7-alpine script: From 113ab81cbf543e118002226eb4d6a8fcbad99619 Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Tue, 2 Mar 2021 09:07:50 +0000 Subject: [PATCH 11/66] Update .gitlab/ci/doc.gitlab-ci.yml --- .gitlab/ci/doc.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 0f21148..07b279f 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -1,5 +1,5 @@ pages: - needs: ["build","deploy"] + needs: ["deploy"] stage: doc image: python:3.7-alpine script: From 5e29c6fe2694a2b0ee3e51a7406867e449c94280 Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Tue, 2 Mar 2021 09:08:39 +0000 Subject: [PATCH 12/66] Update .gitlab/ci/doc.gitlab-ci.yml --- .gitlab/ci/doc.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 07b279f..5490874 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -1,6 +1,6 @@ pages: - needs: ["deploy"] stage: doc + needs: ["deploy"] image: python:3.7-alpine script: - pip install -U sphinx sphinx_rtd_theme From 73b763d85f5260c603275175ac87acedc9c98d18 Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Tue, 2 Mar 2021 09:09:50 +0000 Subject: [PATCH 13/66] fix: job ordering error --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36d3def..c7c6485 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,8 @@ stages: - test - build - - doc - deploy + - doc From 4726a90a4acc5eefd6e97096f56ebf26f92a1b7a Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 2 Mar 2021 10:16:59 +0100 Subject: [PATCH 14/66] doc: reflect doc hosting changes to the Readme.md --- README.md | 66 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 38ab491..d2c8991 100644 --- a/README.md +++ b/README.md @@ -19,44 +19,46 @@ This tool aims to allow multiple users to work on the same scene over the networ ## Usage -See the [documentation](https://multi-user.readthedocs.io/en/latest/) for details. +See the [documentation](https://slumber.gitlab.io/multi-user/index.html) for details. ## Troubleshooting -See the [troubleshooting guide](https://multi-user.readthedocs.io/en/latest/getting_started/troubleshooting.html) for tips on the most common issues. +See the [troubleshooting guide](https://slumber.gitlab.io/multi-user/getting_started/troubleshooting.html) for tips on the most common issues. ## Current development status Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones. -| Name | Status | Comment | -| ----------- | :----: | :--------------------------------------------------------------------------: | -| action | ✔️ | | -| armature | ❗ | Not stable | -| camera | ✔️ | | -| collection | ✔️ | | -| curve | ❗ | Nurbs not supported | -| gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) | -| image | ✔️ | | -| mesh | ✔️ | | -| material | ✔️ | | -| node_groups | ❗ | Material only | -| metaball | ✔️ | | -| object | ✔️ | | -| textures | ❗ | Supported for modifiers only | -| texts | ✔️ | | -| scene | ✔️ | | -| world | ✔️ | | -| lightprobes | ✔️ | | -| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) | -| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) | -| nla | ❌ | | -| 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 | -| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) | -| libraries | ❗ | Partial | +| Name | Status | Comment | +| -------------- | :----: | :--------------------------------------------------------------------------: | +| action | ✔️ | | +| armature | ❗ | Not stable | +| camera | ✔️ | | +| collection | ✔️ | | +| curve | ❗ | Nurbs surfaces not supported | +| gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) | +| image | ✔️ | | +| mesh | ✔️ | | +| material | ✔️ | | +| node_groups | ❗ | Material only | +| geometry nodes | ✔️ | | +| metaball | ✔️ | | +| object | ✔️ | | +| textures | ❗ | Supported for modifiers/materials only | +| texts | ✔️ | | +| scene | ✔️ | | +| world | ✔️ | | +| lightprobes | ✔️ | | +| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) | +| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) | +| nla | ❌ | | +| 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 | +| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) | +| libraries | ❗ | Partial | + ### Performance issues @@ -68,13 +70,13 @@ I'm working on it. | Dependencies | Version | Needed | | ------------ | :-----: | -----: | -| Replication | latest | yes | +| Replication | latest | yes | ## Contributing -See [contributing section](https://multi-user.readthedocs.io/en/latest/ways_to_contribute.html) of the documentation. +See [contributing section](https://slumber.gitlab.io/multi-user/ways_to_contribute.html) of the documentation. Feel free to [join the discord server](https://discord.gg/aBPvGws) to chat, seek help and contribute. From 3d9c78c2f997af3eac1d6c0b669d2f075ebf7562 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 2 Mar 2021 10:18:11 +0100 Subject: [PATCH 15/66] doc: only build for master/develop --- .gitlab/ci/doc.gitlab-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 5490874..a6ae794 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -8,5 +8,9 @@ pages: artifacts: paths: - public + only: + refs: + - master + - develop From 1c3394ce563bb430a0dc0393c7abeb67c29a8c9d Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Tue, 2 Mar 2021 12:46:26 +0000 Subject: [PATCH 16/66] feat: sphinx-material theme --- .gitlab/ci/doc.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index a6ae794..e8b8313 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -3,7 +3,7 @@ pages: needs: ["deploy"] image: python:3.7-alpine script: - - pip install -U sphinx sphinx_rtd_theme + - pip install -U sphinx sphinx_rtd_theme sphinx-material - sphinx-build -b html ./docs public artifacts: paths: From 0ccd0563ea3d52966a1567bbe7553990c63db973 Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Tue, 2 Mar 2021 12:56:12 +0000 Subject: [PATCH 17/66] feat: testing doc building with python 3.8 --- .gitlab/ci/doc.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index e8b8313..8603f57 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -1,7 +1,7 @@ pages: stage: doc needs: ["deploy"] - image: python:3.7-alpine + image: python script: - pip install -U sphinx sphinx_rtd_theme sphinx-material - sphinx-build -b html ./docs public From d0e80da945a8f0ed6e584b64a449efa917873217 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 3 Mar 2021 09:55:48 +0100 Subject: [PATCH 18/66] fix: object parenting can't be removed Related to #179 --- multi_user/bl_types/bl_object.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 810eb69..430cd7c 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -296,9 +296,14 @@ class BlObject(BlDatablock): # Load transformation data loader.load(target, data) + # Object display fields if 'display' in data: loader.load(target.display, data['display']) + # Parent + if 'parent' not in data and target.parent: + target.parent = None + # Pose if 'pose' in data: if not target.pose: @@ -367,6 +372,9 @@ class BlObject(BlDatablock): "name", "rotation_mode", "parent", + "parent_type", + "track_axis", + "up_axis", "data", "library", "empty_display_type", From 19c56e590b2047266e3b6e9852e6baf1f9cac477 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 3 Mar 2021 10:03:57 +0100 Subject: [PATCH 19/66] feat: remove parent as node dependency --- multi_user/bl_types/bl_object.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 430cd7c..b68b24d 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -557,8 +557,6 @@ class BlObject(BlDatablock): # Avoid Empty case if self.instance.data: deps.append(self.instance.data) - if self.instance.parent : - deps.append(self.instance.parent) if self.is_library: deps.append(self.instance.library) From cc5a87adb839c9d93679ad540b8aab65f0483059 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 3 Mar 2021 11:00:47 +0100 Subject: [PATCH 20/66] fix: prevent matrix_parent_inverse from being reset by loading parents only if its necessary --- multi_user/bl_types/bl_object.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index b68b24d..d7dde74 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -300,8 +300,14 @@ class BlObject(BlDatablock): if 'display' in data: loader.load(target.display, data['display']) - # Parent - if 'parent' not in data and target.parent: + # Parenting + parent_id = data.get('parent_id') + if parent_id: + parent = bpy.data.objects[parent_id] + # Avoid reloading + if target.parent != parent and parent is not None: + target.parent = parent + elif target.parent: target.parent = None # Pose @@ -371,10 +377,6 @@ class BlObject(BlDatablock): dumper.include_filter = [ "name", "rotation_mode", - "parent", - "parent_type", - "track_axis", - "up_axis", "data", "library", "empty_display_type", @@ -419,6 +421,10 @@ class BlObject(BlDatablock): if self.is_library: return data + # PARENTING + if instance.parent: + data['parent_id'] = instance.parent.name + # MODIFIERS if hasattr(instance, 'modifiers'): data["modifiers"] = {} @@ -561,6 +567,9 @@ class BlObject(BlDatablock): if self.is_library: deps.append(self.instance.library) + if self.instance.parent : + deps.append(self.instance.parent) + if self.instance.instance_type == 'COLLECTION': # TODO: uuid based deps.append(self.instance.instance_collection) From 2d638ef76fa7d4eb70fa2684283ff030e9a4bfc4 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 4 Mar 2021 14:22:54 +0100 Subject: [PATCH 21/66] refactor: interface api changes --- multi_user/bl_types/__init__.py | 2 +- multi_user/operators.py | 57 ++++++++++++++++----------------- multi_user/presence.py | 2 +- multi_user/timers.py | 14 +++++--- multi_user/ui.py | 36 ++++++++++----------- 5 files changed, 56 insertions(+), 55 deletions(-) diff --git a/multi_user/bl_types/__init__.py b/multi_user/bl_types/__init__.py index 96a5e38..0e8871f 100644 --- a/multi_user/bl_types/__init__.py +++ b/multi_user/bl_types/__init__.py @@ -48,7 +48,7 @@ if bpy.app.version[1] >= 91: __all__.append('bl_volume') from . import * -from replication.data import ReplicatedDataFactory +from replication.data import DataTranslationProtocol def types_to_register(): return __all__ diff --git a/multi_user/operators.py b/multi_user/operators.py index a71d666..e81b56e 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -44,9 +44,10 @@ from bpy.app.handlers import persistent 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.data import DataTranslationProtocol +from replication.repository import Repository from replication.exception import NonAuthorizedOperationError, ContextError -from replication.interface import session +from replication.interface import session, add from . import bl_types, environment, timers, ui, utils from .presence import SessionStatusWidget, renderer, view3d_find @@ -186,7 +187,7 @@ class SessionStartOperator(bpy.types.Operator): handler.setFormatter(formatter) - bpy_factory = ReplicatedDataFactory() + bpy_protocol = DataTranslationProtocol() supported_bl_types = [] # init the factory with supported types @@ -205,7 +206,7 @@ class SessionStartOperator(bpy.types.Operator): type_local_config = settings.supported_datablocks[type_impl_name] - bpy_factory.register_type( + bpy_protocol.register_type( type_module_class.bl_class, type_module_class, check_common=type_module_class.bl_check_common) @@ -217,10 +218,7 @@ class SessionStartOperator(bpy.types.Operator): else: python_binary_path = bpy.app.binary_path_python - session.configure( - factory=bpy_factory, - python_path=python_binary_path, - external_update_handling=True) + repo = Repository(data_protocol=bpy_protocol) # Host a session if self.host: @@ -231,13 +229,14 @@ class SessionStartOperator(bpy.types.Operator): runtime_settings.internet_ip = environment.get_ip() try: + # Init repository for scene in bpy.data.scenes: - session.add(scene) + add(repo, scene) session.host( + repository= repo, id=settings.username, port=settings.port, - ipc_port=settings.ipc_port, timeout=settings.connection_timeout, password=admin_pass, cache_directory=settings.cache_directory, @@ -258,10 +257,10 @@ class SessionStartOperator(bpy.types.Operator): try: session.connect( + repository= repo, id=settings.username, address=settings.ip, port=settings.port, - ipc_port=settings.ipc_port, timeout=settings.connection_timeout, password=admin_pass ) @@ -272,15 +271,13 @@ 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 - # )) session_update = timers.SessionStatusUpdate() session_user_sync = timers.SessionUserSync() session_background_executor = timers.MainThreadExecutor( execution_queue=background_execution_queue) + session_listen = timers.SessionListenTimer() + session_listen.register() session_update.register() session_user_sync.register() session_background_executor.register() @@ -288,7 +285,7 @@ class SessionStartOperator(bpy.types.Operator): deleyables.append(session_background_executor) deleyables.append(session_update) deleyables.append(session_user_sync) - + deleyables.append(session_listen) self.report( @@ -650,7 +647,7 @@ class ApplyArmatureOperator(bpy.types.Operator): return {'CANCELLED'} if event.type == 'TIMER': - if session and session.state['STATE'] == STATE_ACTIVE: + if session and session.state == STATE_ACTIVE: nodes = session.list(filter=bl_types.bl_armature.BlArmature) for node in nodes: @@ -795,7 +792,7 @@ class SessionSaveBackupOperator(bpy.types.Operator, ExportHelper): @classmethod def poll(cls, context): - return session.state['STATE'] == STATE_ACTIVE + return session.state == STATE_ACTIVE class SessionStopAutoSaveOperator(bpy.types.Operator): bl_idname = "session.cancel_autosave" @@ -804,7 +801,7 @@ class SessionStopAutoSaveOperator(bpy.types.Operator): @classmethod def poll(cls, context): - return (session.state['STATE'] == STATE_ACTIVE and 'SessionBackupTimer' in registry) + return (session.state == STATE_ACTIVE and 'SessionBackupTimer' in registry) def execute(self, context): autosave_timer = registry.get('SessionBackupTimer') @@ -829,7 +826,7 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper): ) def execute(self, context): - from replication.graph import ReplicationGraph + from replication.repository import Repository # TODO: add filechecks @@ -849,7 +846,7 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper): # init the factory with supported types - bpy_factory = ReplicatedDataFactory() + bpy_protocol = DataTranslationProtocol() for type in bl_types.types_to_register(): type_module = getattr(bl_types, type) name = [e.capitalize() for e in type.split('_')[1:]] @@ -857,16 +854,16 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper): type_module_class = getattr(type_module, type_impl_name) - bpy_factory.register_type( + bpy_protocol.register_type( type_module_class.bl_class, type_module_class) - graph = ReplicationGraph() + graph = Repository() for node, node_data in nodes: node_type = node_data.get('str_type') - impl = bpy_factory.get_implementation_from_net(node_type) + impl = bpy_protocol.get_implementation_from_net(node_type) if impl: logging.info(f"Loading {node}") @@ -932,7 +929,7 @@ def update_external_dependencies(): def sanitize_deps_graph(remove_nodes: bool = False): """ Cleanup the replication graph """ - if session and session.state['STATE'] == STATE_ACTIVE: + if session and session.state == STATE_ACTIVE: start = utils.current_milli_time() rm_cpt = 0 for node_key in session.list(): @@ -957,18 +954,18 @@ def resolve_deps_graph(dummy): A future solution should be to avoid storing dataclock reference... """ - if session and session.state['STATE'] == STATE_ACTIVE: + if session and session.state == STATE_ACTIVE: sanitize_deps_graph(remove_nodes=True) @persistent def load_pre_handler(dummy): - if session and session.state['STATE'] in [STATE_ACTIVE, STATE_SYNCING]: + if session and session.state in [STATE_ACTIVE, STATE_SYNCING]: bpy.ops.session.stop() @persistent def update_client_frame(scene): - if session and session.state['STATE'] == STATE_ACTIVE: + if session and session.state == STATE_ACTIVE: session.update_user_metadata({ 'frame_current': scene.frame_current }) @@ -976,7 +973,7 @@ def update_client_frame(scene): @persistent def depsgraph_evaluation(scene): - if session and session.state['STATE'] == STATE_ACTIVE: + if session and session.state == STATE_ACTIVE: context = bpy.context blender_depsgraph = bpy.context.view_layer.depsgraph dependency_updates = [u for u in blender_depsgraph.updates] @@ -1035,7 +1032,7 @@ def register(): def unregister(): - if session and session.state['STATE'] == STATE_ACTIVE: + if session and session.state == STATE_ACTIVE: session.disconnect() from bpy.utils import unregister_class diff --git a/multi_user/presence.py b/multi_user/presence.py index 4776e03..f89844d 100644 --- a/multi_user/presence.py +++ b/multi_user/presence.py @@ -399,7 +399,7 @@ class SessionStatusWidget(Widget): 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 = session.state state_str = f"{get_state_str(state)}" if state == STATE_ACTIVE: diff --git a/multi_user/timers.py b/multi_user/timers.py index 9e1d2e6..99af20a 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -23,7 +23,7 @@ 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, ContextError -from replication.interface import session +from replication.interface import session, add from . import operators, utils from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget, @@ -71,7 +71,7 @@ class Timer(object): except Exception as e: logging.error(e) self.unregister() - session.disconnect() + session.disconnect(reason=f"Error during timer {self.id} execution") else: if self.is_running: return self._timeout @@ -100,9 +100,13 @@ class SessionBackupTimer(Timer): def execute(self): session.save(self._filepath) +class SessionListenTimer(Timer): + def execute(self): + session.listen() + class ApplyTimer(Timer): def execute(self): - if session and session.state['STATE'] == STATE_ACTIVE: + if session and session.state == STATE_ACTIVE: nodes = session.list() for node in nodes: @@ -130,7 +134,7 @@ class DynamicRightSelectTimer(Timer): def execute(self): settings = utils.get_preferences() - if session and session.state['STATE'] == STATE_ACTIVE: + if session and session.state == STATE_ACTIVE: # Find user if self._user is None: self._user = session.online_users.get(settings.username) @@ -262,7 +266,7 @@ class ClientUpdate(Timer): settings = utils.get_preferences() if session and renderer: - if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]: + if session.state in [STATE_ACTIVE, STATE_LOBBY]: local_user = session.online_users.get( settings.username) diff --git a/multi_user/ui.py b/multi_user/ui.py index b8f423b..abc2a18 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -71,9 +71,9 @@ class SESSION_PT_settings(bpy.types.Panel): def draw_header(self, context): layout = self.layout - if session and session.state['STATE'] != STATE_INITIAL: + if session and session.state != STATE_INITIAL: cli_state = session.state - state = session.state.get('STATE') + state = session.state connection_icon = "KEYTYPE_MOVING_HOLD_VEC" if state == STATE_ACTIVE: @@ -81,7 +81,7 @@ class SESSION_PT_settings(bpy.types.Panel): else: connection_icon = 'PROP_CON' - layout.label(text=f"Session - {get_state_str(cli_state['STATE'])}", icon=connection_icon) + layout.label(text=f"Session - {get_state_str(cli_state)}", icon=connection_icon) else: layout.label(text=f"Session - v{__version__}",icon="PROP_OFF") @@ -94,13 +94,13 @@ class SESSION_PT_settings(bpy.types.Panel): if hasattr(context.window_manager, 'session'): # STATE INITIAL if not session \ - or (session and session.state['STATE'] == STATE_INITIAL): + or (session and session.state == STATE_INITIAL): pass else: - cli_state = session.state + progress = session.state_progress row = layout.row() - current_state = cli_state['STATE'] + current_state = session.state info_msg = None if current_state in [STATE_ACTIVE]: @@ -124,8 +124,8 @@ class SESSION_PT_settings(bpy.types.Panel): if current_state in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]: info_box = row.box() info_box.row().label(text=printProgressBar( - cli_state['CURRENT'], - cli_state['TOTAL'], + progress['current'], + progress['total'], length=16 )) @@ -141,7 +141,7 @@ class SESSION_PT_settings_network(bpy.types.Panel): @classmethod def poll(cls, context): return not session \ - or (session and session.state['STATE'] == 0) + or (session and session.state == 0) def draw_header(self, context): self.layout.label(text="", icon='URL') @@ -199,7 +199,7 @@ class SESSION_PT_settings_user(bpy.types.Panel): @classmethod def poll(cls, context): return not session \ - or (session and session.state['STATE'] == 0) + or (session and session.state == 0) def draw_header(self, context): self.layout.label(text="", icon='USER') @@ -230,7 +230,7 @@ class SESSION_PT_advanced_settings(bpy.types.Panel): @classmethod def poll(cls, context): return not session \ - or (session and session.state['STATE'] == 0) + or (session and session.state == 0) def draw_header(self, context): self.layout.label(text="", icon='PREFERENCES') @@ -322,7 +322,7 @@ class SESSION_PT_user(bpy.types.Panel): @classmethod def poll(cls, context): - return session and session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY] + return session and session.state in [STATE_ACTIVE, STATE_LOBBY] def draw_header(self, context): self.layout.label(text="", icon='USER') @@ -353,7 +353,7 @@ class SESSION_PT_user(bpy.types.Panel): if active_user != 0 and active_user.username != settings.username: row = layout.row() user_operations = row.split() - if session.state['STATE'] == STATE_ACTIVE: + if session.state == STATE_ACTIVE: user_operations.alert = context.window_manager.session.time_snap_running user_operations.operator( @@ -411,7 +411,7 @@ class SESSION_PT_presence(bpy.types.Panel): @classmethod def poll(cls, context): return not session \ - or (session and session.state['STATE'] in [STATE_INITIAL, STATE_ACTIVE]) + or (session and session.state in [STATE_INITIAL, STATE_ACTIVE]) def draw_header(self, context): self.layout.prop(context.window_manager.session, @@ -519,8 +519,8 @@ class SESSION_PT_repository(bpy.types.Panel): admin = usr['admin'] return hasattr(context.window_manager, 'session') and \ session and \ - (session.state['STATE'] == STATE_ACTIVE or \ - session.state['STATE'] == STATE_LOBBY and admin) + (session.state == STATE_ACTIVE or \ + session.state == STATE_LOBBY and admin) def draw_header(self, context): self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE') @@ -536,7 +536,7 @@ class SESSION_PT_repository(bpy.types.Panel): row = layout.row() - if session.state['STATE'] == STATE_ACTIVE: + if session.state == STATE_ACTIVE: if 'SessionBackupTimer' in registry: row.alert = True row.operator('session.cancel_autosave', icon="CANCEL") @@ -579,7 +579,7 @@ class SESSION_PT_repository(bpy.types.Panel): else: row.label(text="Empty") - elif session.state['STATE'] == STATE_LOBBY and usr and usr['admin']: + elif session.state == STATE_LOBBY and usr and usr['admin']: row.operator("session.init", icon='TOOL_SETTINGS', text="Init") else: row.label(text="Waiting to start") From 875b9ce934d55cd2442eb111d754f20feeb804cd Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 4 Mar 2021 14:24:03 +0100 Subject: [PATCH 22/66] feat: temporary disable CI jobs for this branch because of breaking changes --- .gitlab-ci.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index c7c6485..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,13 +0,0 @@ -stages: - - test - - build - - deploy - - doc - - - -include: - - local: .gitlab/ci/test.gitlab-ci.yml - - local: .gitlab/ci/build.gitlab-ci.yml - - local: .gitlab/ci/deploy.gitlab-ci.yml - - local: .gitlab/ci/doc.gitlab-ci.yml From 94877533070e33f2946a1b97f514a29edc9bb54f Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 4 Mar 2021 15:48:36 +0100 Subject: [PATCH 23/66] feat: fix object and collection support for geometry nodes --- multi_user/bl_types/bl_material.py | 120 ++++++++++++++++------------- 1 file changed, 65 insertions(+), 55 deletions(-) diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index e815831..12964ed 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -27,7 +27,7 @@ from .dump_anything import Loader, Dumper from .bl_datablock import BlDatablock, get_datablock_from_uuid NODE_SOCKET_INDEX = re.compile('\[(\d*)\]') -IGNORED_SOCKETS = ['GEOMETRY'] +IGNORED_SOCKETS = ['GEOMETRY', 'SHADER'] def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): """ Load a node into a node_tree from a dict @@ -53,13 +53,16 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): inputs_data = node_data.get('inputs') if inputs_data: inputs = [i for i in target_node.inputs if i.type not in IGNORED_SOCKETS] - - for idx, inpt in enumerate(inputs_data): - if idx < len(inputs) and hasattr(inputs[idx], "default_value"): + for idx, inpt in enumerate(inputs): + loaded_input = inputs_data[idx] + if idx < len(inputs_data) and hasattr(inpt, "default_value"): try: - inputs[idx].default_value = inpt + if inpt.type in ['OBJECT', 'COLLECTION']: + inpt.default_value = get_datablock_from_uuid(loaded_input, None) + else: + inpt.default_value = loaded_input except Exception as e: - logging.warning(f"Node {target_node.name} input {inputs[idx].name} parameter not supported, skipping ({e})") + logging.warning(f"Node {target_node.name} input {inpt.name} parameter not supported, skipping ({e})") else: logging.warning(f"Node {target_node.name} input length mismatch.") @@ -78,48 +81,6 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): f"Node {target_node.name} output length mismatch.") -def load_links(links_data, node_tree): - """ Load node_tree links from a list - - :arg links_data: dumped node links - :type links_data: list - :arg node_tree: node links collection - :type node_tree: bpy.types.NodeTree - """ - - for link in links_data: - input_socket = node_tree.nodes[link['to_node'] - ].inputs[int(link['to_socket'])] - output_socket = node_tree.nodes[link['from_node']].outputs[int( - link['from_socket'])] - node_tree.links.new(input_socket, output_socket) - - -def dump_links(links): - """ Dump node_tree links collection to a list - - :arg links: node links collection - :type links: bpy.types.NodeLinks - :retrun: list - """ - - links_data = [] - - for link in links: - to_socket = NODE_SOCKET_INDEX.search( - link.to_socket.path_from_id()).group(1) - from_socket = NODE_SOCKET_INDEX.search( - link.from_socket.path_from_id()).group(1) - links_data.append({ - 'to_node': link.to_node.name, - 'to_socket': to_socket, - 'from_node': link.from_node.name, - 'from_socket': from_socket, - }) - - return links_data - - def dump_node(node: bpy.types.ShaderNode) -> dict: """ Dump a single node to a dict @@ -167,17 +128,23 @@ def dump_node(node: bpy.types.ShaderNode) -> dict: if hasattr(node, 'inputs'): dumped_node['inputs'] = [] - for idx, inpt in enumerate(node.inputs): + inputs = [i for i in node.inputs if i.type not in IGNORED_SOCKETS] + for idx, inpt in enumerate(inputs): if hasattr(inpt, 'default_value'): - dumped_node['inputs'].append( - io_dumper.dump(inpt.default_value)) + if isinstance(inpt.default_value, bpy.types.ID): + dumped_input = inpt.default_value.uuid + else: + dumped_input = io_dumper.dump(inpt.default_value) + + dumped_node['inputs'].append(dumped_input) 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)) + if output.type not in IGNORED_SOCKETS: + if hasattr(output, 'default_value'): + dumped_node['outputs'].append( + io_dumper.dump(output.default_value)) if hasattr(node, 'color_ramp'): ramp_dumper = Dumper() @@ -207,6 +174,49 @@ def dump_node(node: bpy.types.ShaderNode) -> dict: return dumped_node + +def load_links(links_data, node_tree): + """ Load node_tree links from a list + + :arg links_data: dumped node links + :type links_data: list + :arg node_tree: node links collection + :type node_tree: bpy.types.NodeTree + """ + + for link in links_data: + input_socket = node_tree.nodes[link['to_node'] + ].inputs[int(link['to_socket'])] + output_socket = node_tree.nodes[link['from_node']].outputs[int( + link['from_socket'])] + node_tree.links.new(input_socket, output_socket) + + +def dump_links(links): + """ Dump node_tree links collection to a list + + :arg links: node links collection + :type links: bpy.types.NodeLinks + :retrun: list + """ + + links_data = [] + + for link in links: + to_socket = NODE_SOCKET_INDEX.search( + link.to_socket.path_from_id()).group(1) + from_socket = NODE_SOCKET_INDEX.search( + link.from_socket.path_from_id()).group(1) + links_data.append({ + 'to_node': link.to_node.name, + 'to_socket': to_socket, + 'from_node': link.from_node.name, + 'from_socket': from_socket, + }) + + return links_data + + def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict: """ Dump a shader node_tree to a dict including links and nodes @@ -263,7 +273,7 @@ def load_node_tree_sockets(sockets: bpy.types.Collection, """ # Check for removed sockets for socket in sockets: - if not [s for s in sockets_data if socket['uuid'] == s[2]]: + if not [s for s in sockets_data if 'uuid' in socket and socket['uuid'] == s[2]]: sockets.remove(socket) # Check for new sockets From b17104c67e7a4bd2da26d4eccf2e00c8862c0242 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 5 Mar 2021 10:35:35 +0100 Subject: [PATCH 24/66] fix: naming --- multi_user/operators.py | 8 ++++---- multi_user/timers.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index e81b56e..afde378 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -81,7 +81,7 @@ def initialize_session(): # Step 1: Constrect nodes logging.info("Constructing nodes") - for node in session._graph.list_ordered(): + for node in session._repository.list_ordered(): node_ref = session.get(uuid=node) if node_ref is None: logging.error(f"Can't construct node {node}") @@ -90,7 +90,7 @@ def initialize_session(): # Step 2: Load nodes logging.info("Loading nodes") - for node in session._graph.list_ordered(): + for node in session._repository.list_ordered(): node_ref = session.get(uuid=node) if node_ref is None: @@ -275,7 +275,7 @@ class SessionStartOperator(bpy.types.Operator): session_user_sync = timers.SessionUserSync() session_background_executor = timers.MainThreadExecutor( execution_queue=background_execution_queue) - session_listen = timers.SessionListenTimer() + session_listen = timers.SessionListenTimer(timeout=0.001) session_listen.register() session_update.register() @@ -602,7 +602,7 @@ class SessionApply(bpy.types.Operator): force=True, force_dependencies=self.reset_dependencies) if node_ref.bl_reload_parent: - for parent in session._graph.find_parents(self.target): + for parent in session._repository.find_parents(self.target): logging.debug(f"Refresh parent {parent}") session.apply(parent, force=True) except Exception as e: diff --git a/multi_user/timers.py b/multi_user/timers.py index 99af20a..e8c7340 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -119,7 +119,7 @@ class ApplyTimer(Timer): logging.error(f"Fail to apply {node_ref.uuid}: {e}") else: if node_ref.bl_reload_parent: - for parent in session._graph.find_parents(node): + for parent in session._repository.find_parents(node): logging.debug("Refresh parent {node}") session.apply(parent, force=True) From 93df5ca5fa23e98bcebd229c11a6c22d4af72800 Mon Sep 17 00:00:00 2001 From: Swann Date: Sat, 6 Mar 2021 10:20:57 +0100 Subject: [PATCH 25/66] fix: disconnect callback --- multi_user/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index afde378..d10670b 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -348,7 +348,7 @@ class SessionStopOperator(bpy.types.Operator): if session: try: - session.disconnect() + session.disconnect(reason='user') except Exception as e: self.report({'ERROR'}, repr(e)) From 8e3c86561f67f72f6a53bb030573dc7ed630163f Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 9 Mar 2021 10:19:51 +0100 Subject: [PATCH 26/66] refactor: move add to porcelain --- multi_user/operators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index d10670b..b710cf0 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -45,9 +45,10 @@ 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 DataTranslationProtocol +from replication.exception import ContextError, NonAuthorizedOperationError +from replication.interface import session +from replication.porcelain import add from replication.repository import Repository -from replication.exception import NonAuthorizedOperationError, ContextError -from replication.interface import session, add from . import bl_types, environment, timers, ui, utils from .presence import SessionStatusWidget, renderer, view3d_find From 647ac46c011708ce6f014448beade2fae3413833 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 9 Mar 2021 14:07:59 +0100 Subject: [PATCH 27/66] feat: move apply to porcelain feat: move data access to repository feat: object_store layer to repository (with GraphObjectStore) revert: missing network services --- multi_user/operators.py | 37 ++++++++++++++++++++----------------- multi_user/presence.py | 2 +- multi_user/timers.py | 21 +++++++++++---------- multi_user/ui.py | 6 +++--- multi_user/utils.py | 4 ++-- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index b710cf0..907056c 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -47,7 +47,7 @@ from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE, from replication.data import DataTranslationProtocol from replication.exception import ContextError, NonAuthorizedOperationError from replication.interface import session -from replication.porcelain import add +from replication.porcelain import add, apply from replication.repository import Repository from . import bl_types, environment, timers, ui, utils @@ -82,8 +82,8 @@ def initialize_session(): # Step 1: Constrect nodes logging.info("Constructing nodes") - for node in session._repository.list_ordered(): - node_ref = session.get(uuid=node) + for node in session.repository.list_ordered(): + node_ref = session.repository.get_node(node) if node_ref is None: logging.error(f"Can't construct node {node}") elif node_ref.state == FETCHED: @@ -91,8 +91,8 @@ def initialize_session(): # Step 2: Load nodes logging.info("Loading nodes") - for node in session._repository.list_ordered(): - node_ref = session.get(uuid=node) + for node in session.repository.list_ordered(): + node_ref = session.repository.get_node(node) if node_ref is None: logging.error(f"Can't load node {node}") @@ -598,14 +598,17 @@ class SessionApply(bpy.types.Operator): def execute(self, context): logging.debug(f"Running apply on {self.target}") try: - node_ref = session.get(uuid=self.target) - session.apply(self.target, - force=True, - force_dependencies=self.reset_dependencies) + node_ref = session.repository.get_node(self.target) + apply(session.repository, + self.target, + force=True, + force_dependencies=self.reset_dependencies) if node_ref.bl_reload_parent: - for parent in session._repository.find_parents(self.target): + for parent in session.repository.find_parents(self.target): logging.debug(f"Refresh parent {parent}") - session.apply(parent, force=True) + apply(session.repository, + parent, + force=True) except Exception as e: self.report({'ERROR'}, repr(e)) return {"CANCELED"} @@ -652,11 +655,11 @@ class ApplyArmatureOperator(bpy.types.Operator): nodes = session.list(filter=bl_types.bl_armature.BlArmature) for node in nodes: - node_ref = session.get(uuid=node) + node_ref = session.repository.get_node(node) if node_ref.state == FETCHED: try: - session.apply(node) + apply(session.repository, node) except Exception as e: logging.error("Fail to apply armature: {e}") @@ -921,7 +924,7 @@ classes = ( 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) + node = session.repository.get_node(node_id) if node and node.owner in [session.id, RP_COMMON] \ and node.has_changed(): session.commit(node_id) @@ -934,7 +937,7 @@ def sanitize_deps_graph(remove_nodes: bool = False): start = utils.current_milli_time() rm_cpt = 0 for node_key in session.list(): - node = session.get(node_key) + node = session.repository.get_node(node_key) if node is None \ or (node.state == UP and not node.resolve(construct=False)): if remove_nodes: @@ -987,7 +990,7 @@ def depsgraph_evaluation(scene): # Is the object tracked ? if update.id.uuid: # Retrieve local version - node = session.get(uuid=update.id.uuid) + node = session.repository.get_node(update.id.uuid) # Check our right on this update: # - if its ours or ( under common and diff), launch the @@ -1011,7 +1014,7 @@ def depsgraph_evaluation(scene): continue # A new scene is created elif isinstance(update.id, bpy.types.Scene): - ref = session.get(reference=update.id) + ref = session.repository.get_node_by_datablock(update.id) if ref: ref.resolve() else: diff --git a/multi_user/presence.py b/multi_user/presence.py index f89844d..30b10d2 100644 --- a/multi_user/presence.py +++ b/multi_user/presence.py @@ -30,7 +30,7 @@ import mathutils from bpy_extras import view3d_utils from gpu_extras.batch import batch_for_shader from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG, - STATE_INITIAL, STATE_LAUNCHING_SERVICES, + STATE_INITIAL, CONNECTING, STATE_LOBBY, STATE_QUITTING, STATE_SRV_SYNC, STATE_SYNCING, STATE_WAITING) from replication.interface import session diff --git a/multi_user/timers.py b/multi_user/timers.py index e8c7340..9ec9ca4 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -23,7 +23,8 @@ 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, ContextError -from replication.interface import session, add +from replication.interface import session +from replication.porcelain import apply, add from . import operators, utils from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget, @@ -110,18 +111,18 @@ class ApplyTimer(Timer): nodes = session.list() for node in nodes: - node_ref = session.get(uuid=node) + node_ref = session.repository.get_node(node) if node_ref.state == FETCHED: try: - session.apply(node) + apply(session.repository, node) except Exception as e: logging.error(f"Fail to apply {node_ref.uuid}: {e}") else: if node_ref.bl_reload_parent: - for parent in session._repository.find_parents(node): + for parent in session.repository.find_parents(node): logging.debug("Refresh parent {node}") - session.apply(parent, force=True) + apply(session.repository, parent, force=True) class DynamicRightSelectTimer(Timer): @@ -148,7 +149,7 @@ class DynamicRightSelectTimer(Timer): # if an annotation exist and is tracked if annotation_gp and annotation_gp.uuid: - registered_gp = session.get(uuid=annotation_gp.uuid) + registered_gp = session.repository.get_node(annotation_gp.uuid) if is_annotating(bpy.context): # try to get the right on it if registered_gp.owner == RP_COMMON: @@ -162,7 +163,7 @@ class DynamicRightSelectTimer(Timer): affect_dependencies=False) if registered_gp.owner == settings.username: - gp_node = session.get(uuid=annotation_gp.uuid) + gp_node = session.repository.get_node(annotation_gp.uuid) if gp_node.has_changed(): session.commit(gp_node.uuid) session.push(gp_node.uuid, check_data=False) @@ -186,7 +187,7 @@ class DynamicRightSelectTimer(Timer): # change old selection right to common for obj in obj_common: - node = session.get(uuid=obj) + node = session.repository.get_node(obj) if node and (node.owner == settings.username or node.owner == RP_COMMON): recursive = True @@ -204,7 +205,7 @@ class DynamicRightSelectTimer(Timer): # change new selection to our for obj in obj_ours: - node = session.get(uuid=obj) + node = session.repository.get_node(obj) if node and node.owner == RP_COMMON: recursive = True @@ -237,7 +238,7 @@ class DynamicRightSelectTimer(Timer): owned_keys = session.list( filter_owner=settings.username) for key in owned_keys: - node = session.get(uuid=key) + node = session.repository.get_node(key) try: session.change_owner( key, diff --git a/multi_user/ui.py b/multi_user/ui.py index abc2a18..f956d0f 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -26,7 +26,7 @@ from replication.constants import (ADDED, ERROR, FETCHED, STATE_INITIAL, STATE_SRV_SYNC, STATE_WAITING, STATE_QUITTING, STATE_LOBBY, - STATE_LAUNCHING_SERVICES) + CONNECTING) from replication import __version__ from replication.interface import session from .timers import registry @@ -441,7 +441,7 @@ class SESSION_PT_presence(bpy.types.Panel): def draw_property(context, parent, property_uuid, level=0): settings = get_preferences() runtime_settings = context.window_manager.session - item = session.get(uuid=property_uuid) + item = session.repository.get_node(property_uuid) area_msg = parent.row(align=True) @@ -568,7 +568,7 @@ class SESSION_PT_repository(bpy.types.Panel): filter_owner=settings.username) if runtime_settings.filter_owned else session.list() client_keys = [key for key in key_to_filter - if session.get(uuid=key).str_type + if session.repository.get_node(key).str_type in types_filter] if client_keys: diff --git a/multi_user/utils.py b/multi_user/utils.py index 25444f9..a593015 100644 --- a/multi_user/utils.py +++ b/multi_user/utils.py @@ -36,7 +36,7 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_INITIAL, STATE_SRV_SYNC, STATE_WAITING, STATE_QUITTING, STATE_LOBBY, - STATE_LAUNCHING_SERVICES) + CONNECTING) def find_from_attr(attr_name, attr_value, list): @@ -92,7 +92,7 @@ def get_state_str(state): state_str = 'OFFLINE' elif state == STATE_QUITTING: state_str = 'QUITTING' - elif state == STATE_LAUNCHING_SERVICES: + elif state == CONNECTING: state_str = 'LAUNCHING SERVICES' elif state == STATE_LOBBY: state_str = 'LOBBY' From 235db712fdde2d2d5e1e326d60de2d162817f3f2 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 11 Mar 2021 15:45:48 +0100 Subject: [PATCH 28/66] fix: api --- multi_user/operators.py | 6 +++--- multi_user/timers.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/multi_user/operators.py b/multi_user/operators.py index 907056c..4399e7f 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -327,7 +327,7 @@ class SessionInitOperator(bpy.types.Operator): utils.clean_scene() for scene in bpy.data.scenes: - session.add(scene) + add(session.repository, scene) session.init() @@ -875,7 +875,7 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper): uuid=node, dependencies=node_data['dependencies'], data=node_data['data']) - instance.store(graph) + graph.do_commit(instance) instance.state = FETCHED logging.info("Graph succefully loaded") @@ -1018,7 +1018,7 @@ def depsgraph_evaluation(scene): if ref: ref.resolve() else: - scn_uuid = session.add(update.id) + scn_uuid = add(session.repository, update.id) session.commit(scn_uuid) session.push(scn_uuid, check_data=False) def register(): diff --git a/multi_user/timers.py b/multi_user/timers.py index 9ec9ca4..c4a1ed8 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -17,6 +17,7 @@ import logging import sys +import traceback import bpy from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE, @@ -117,7 +118,7 @@ class ApplyTimer(Timer): try: apply(session.repository, node) except Exception as e: - logging.error(f"Fail to apply {node_ref.uuid}: {e}") + traceback.print_exc() else: if node_ref.bl_reload_parent: for parent in session.repository.find_parents(node): From b965c80ba5ce867b316fc69633c5710511c4f07e Mon Sep 17 00:00:00 2001 From: Swann Date: Sat, 13 Mar 2021 18:32:20 +0100 Subject: [PATCH 29/66] fix: parent transform fix: race condition for COMMON objects related to #180 --- multi_user/bl_types/bl_object.py | 16 ++++++++++++---- multi_user/operators.py | 2 +- 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 d7dde74..fdb4ba7 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -363,6 +363,12 @@ class BlObject(BlDatablock): for modifier in nodes_modifiers: load_modifier_geometry_node_inputs(data['modifiers'][modifier.name], modifier) + transform = data.get('transforms', None) + if transform: + target.matrix_parent_inverse = mathutils.Matrix(transform['matrix_parent_inverse']) + target.matrix_basis = mathutils.Matrix(transform['matrix_basis']) + target.matrix_local = mathutils.Matrix(transform['matrix_local']) + def _dump_implementation(self, data, instance=None): assert(instance) @@ -391,8 +397,6 @@ class BlObject(BlDatablock): "color", "instance_collection", "instance_type", - "location", - "scale", 'lock_location', 'lock_rotation', 'lock_scale', @@ -406,12 +410,16 @@ class BlObject(BlDatablock): 'show_all_edges', 'show_texture_space', 'show_in_front', - 'type', - 'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler', + 'type' ] data = dumper.dump(instance) + dumper.include_filter = [ + 'matrix_parent_inverse', + 'matrix_local', + 'matrix_basis'] + data['transforms'] = dumper.dump(instance) dumper.include_filter = [ 'show_shadows', ] diff --git a/multi_user/operators.py b/multi_user/operators.py index a71d666..8aa9a0f 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -995,7 +995,7 @@ def depsgraph_evaluation(scene): # - if its ours or ( under common and diff), launch the # update process # - if its to someone else, ignore the update - if node and node.owner in [session.id, RP_COMMON]: + if node and (node.owner == session.id or node.bl_check_common): if node.state == UP: try: if node.has_changed(): From c7e8002fed50994ff366b74007b1469e76112906 Mon Sep 17 00:00:00 2001 From: Swann Date: Sun, 14 Mar 2021 18:32:04 +0100 Subject: [PATCH 30/66] fix: apply api clean: ipc port propertie --- docs/getting_started/quickstart.rst | 9 --------- multi_user/operators.py | 10 ++++++---- multi_user/preferences.py | 14 -------------- multi_user/timers.py | 2 +- multi_user/ui.py | 3 --- 5 files changed, 7 insertions(+), 31 deletions(-) diff --git a/docs/getting_started/quickstart.rst b/docs/getting_started/quickstart.rst index d81eff1..c3092e7 100644 --- a/docs/getting_started/quickstart.rst +++ b/docs/getting_started/quickstart.rst @@ -374,15 +374,6 @@ Network Advanced network settings -**IPC Port** is the port used for Inter Process Communication. This port is used -by the multi-user subprocesses to communicate with each other. If different instances -of multi-user are using the same IPC port, this will create conflict ! - -.. note:: - You only need to modify this setting if you need to launch multiple clients from the same - computer (or if you try to host and join from the same computer). To resolve this, you simply need to enter a different - **IPC port** for each blender instance. - **Timeout (in milliseconds)** is the maximum ping authorized before auto-disconnecting. You should only increase it if you have a bad connection. diff --git a/multi_user/operators.py b/multi_user/operators.py index aea8271..9a99097 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -32,6 +32,7 @@ from operator import itemgetter from pathlib import Path from queue import Queue from time import gmtime, strftime +import traceback try: import _pickle as pickle @@ -247,7 +248,6 @@ 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: @@ -604,14 +604,16 @@ class SessionApply(bpy.types.Operator): force=True, force_dependencies=self.reset_dependencies) if node_ref.bl_reload_parent: - for parent in session.repository.find_parents(self.target): + for parent in session.repository.get_parents(self.target): logging.debug(f"Refresh parent {parent}") + apply(session.repository, - parent, + parent.uuid, force=True) except Exception as e: self.report({'ERROR'}, repr(e)) - return {"CANCELED"} + traceback.print_exc() + return {"CANCELLED"} return {"FINISHED"} diff --git a/multi_user/preferences.py b/multi_user/preferences.py index 1757c1b..7df200b 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -66,14 +66,6 @@ def update_ip(self, context): self['ip'] = "127.0.0.1" -def update_port(self, context): - max_port = self.port + 3 - - if self.ipc_port < max_port and \ - self['ipc_port'] >= self.port: - logging.error( - "IPC Port in conflict with the port, assigning a random value") - self['ipc_port'] = random.randrange(self.port+4, 10000) def update_directory(self, context): @@ -174,12 +166,6 @@ class SessionPrefs(bpy.types.AddonPreferences): supported_datablocks: bpy.props.CollectionProperty( type=ReplicatedDatablock, ) - ipc_port: bpy.props.IntProperty( - name="ipc_port", - description='internal ttl port(only useful for multiple local instances)', - default=random.randrange(5570, 70000), - update=update_port, - ) init_method: bpy.props.EnumProperty( name='init_method', description='Init repo', diff --git a/multi_user/timers.py b/multi_user/timers.py index c4a1ed8..4b99b68 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -121,7 +121,7 @@ class ApplyTimer(Timer): traceback.print_exc() else: if node_ref.bl_reload_parent: - for parent in session.repository.find_parents(node): + for parent in session.repository.get_parents(node): logging.debug("Refresh parent {node}") apply(session.repository, parent, force=True) diff --git a/multi_user/ui.py b/multi_user/ui.py index f956d0f..df5ae13 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -251,9 +251,6 @@ class SESSION_PT_advanced_settings(bpy.types.Panel): emboss=False) if settings.sidebar_advanced_net_expanded: - net_section_row = net_section.row() - net_section_row.label(text="IPC Port:") - net_section_row.prop(settings, "ipc_port", text="") net_section_row = net_section.row() net_section_row.label(text="Timeout (ms):") net_section_row.prop(settings, "connection_timeout", text="") From 3a02711baa4d8d0eafb330a2c76a130e534fb60c Mon Sep 17 00:00:00 2001 From: Swann Date: Sun, 14 Mar 2021 20:58:25 +0100 Subject: [PATCH 31/66] feat: faster root management --- multi_user/bl_types/bl_scene.py | 2 ++ multi_user/timers.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_scene.py b/multi_user/bl_types/bl_scene.py index 08a3d69..aef762a 100644 --- a/multi_user/bl_types/bl_scene.py +++ b/multi_user/bl_types/bl_scene.py @@ -368,6 +368,8 @@ def load_sequence(sequence_data: dict, sequence_editor: bpy.types.SequenceEditor class BlScene(BlDatablock): + is_root = True + bl_id = "scenes" bl_class = bpy.types.Scene bl_check_common = True diff --git a/multi_user/timers.py b/multi_user/timers.py index 4b99b68..22e6a64 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -123,7 +123,9 @@ class ApplyTimer(Timer): if node_ref.bl_reload_parent: for parent in session.repository.get_parents(node): logging.debug("Refresh parent {node}") - apply(session.repository, parent, force=True) + apply(session.repository, + parent.uuid, + force=True) class DynamicRightSelectTimer(Timer): From 07862f1cf02837acb195e2b60476875f4ac6be7a Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 19 Mar 2021 11:07:04 +0100 Subject: [PATCH 32/66] fix: missing hue_interpolation --- multi_user/bl_types/bl_material.py | 1 + 1 file changed, 1 insertion(+) diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 12964ed..1d3b57f 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -155,6 +155,7 @@ def dump_node(node: bpy.types.ShaderNode) -> dict: 'color', 'position', 'interpolation', + 'hue_interpolation', 'color_mode' ] dumped_node['color_ramp'] = ramp_dumper.dump(node.color_ramp) From 2446df4fe33995ae54f5f971fce27e8d2c82c44e Mon Sep 17 00:00:00 2001 From: Swann Date: Sun, 21 Mar 2021 09:28:54 +0100 Subject: [PATCH 33/66] feat: raise the default timeout to 5 second --- multi_user/preferences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/preferences.py b/multi_user/preferences.py index 1757c1b..790af67 100644 --- a/multi_user/preferences.py +++ b/multi_user/preferences.py @@ -195,7 +195,7 @@ class SessionPrefs(bpy.types.AddonPreferences): connection_timeout: bpy.props.IntProperty( name='connection timeout', description='connection timeout before disconnection', - default=1000 + default=5000 ) # Replication update settings depsgraph_update_rate: bpy.props.FloatProperty( From 9d0d684589215237bc08e814de5db43081708097 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 31 Mar 2021 11:19:03 +0200 Subject: [PATCH 34/66] fix: geometry nodes str, float, int loading --- multi_user/bl_types/bl_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index fdb4ba7..b3c787e 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -79,7 +79,7 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b dumped_value = dumped_modifier['inputs'][input_index] input_value = target_modifier[input_name] if type(input_value) in [int, str, float]: - input_value = dumped_value + target_modifier[input_name] = dumped_value elif hasattr(input_value, 'to_list'): for index in range(len(input_value)): input_value[index] = dumped_value[index] From 67d18f08e2977ff66230a4eaa1854aaea70215f6 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 31 Mar 2021 15:38:35 +0200 Subject: [PATCH 35/66] fix: Timer not unregistered error fix: handle correctly unsupported float parameter for geometry nodes fix: Material loading --- multi_user/bl_types/bl_material.py | 5 +---- multi_user/bl_types/bl_object.py | 8 ++++++-- multi_user/operators.py | 3 +-- multi_user/timers.py | 5 +++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 1d3b57f..3dc82ca 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -365,10 +365,7 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_ 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") + mat_ref = bpy.data.materials[mat_name] dst_materials.append(mat_ref) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index b3c787e..3f72d39 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -55,7 +55,9 @@ def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list: dumped_input = None if isinstance(input_value, bpy.types.ID): dumped_input = input_value.uuid - elif type(input_value) in [int, str, float]: + elif isinstance(input_value, float): + logging.warning("Float parameter not supported in blender 2.92, skipping it") + elif isinstance(input_value,(int, str)): dumped_input = input_value elif hasattr(input_value, 'to_list'): dumped_input = input_value.to_list() @@ -78,7 +80,9 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b for input_index, input_name in enumerate(inputs_name): dumped_value = dumped_modifier['inputs'][input_index] input_value = target_modifier[input_name] - if type(input_value) in [int, str, float]: + if isinstance(input_value, float): + logging.warning("Float parameter not supported in blender 2.92, skipping it") + elif isinstance(input_value,(int, str)): target_modifier[input_name] = dumped_value elif hasattr(input_value, 'to_list'): for index in range(len(input_value)): diff --git a/multi_user/operators.py b/multi_user/operators.py index 8aa9a0f..981c7df 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -210,8 +210,6 @@ class SessionStartOperator(bpy.types.Operator): type_module_class, check_common=type_module_class.bl_check_common) - deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate)) - if bpy.app.version[1] >= 91: python_binary_path = sys.executable else: @@ -272,6 +270,7 @@ class SessionStartOperator(bpy.types.Operator): # Background client updates service deleyables.append(timers.ClientUpdate()) deleyables.append(timers.DynamicRightSelectTimer()) + deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate)) # deleyables.append(timers.PushTimer( # queue=stagging, # timeout=settings.depsgraph_update_rate diff --git a/multi_user/timers.py b/multi_user/timers.py index 9e1d2e6..1e6c64d 100644 --- a/multi_user/timers.py +++ b/multi_user/timers.py @@ -17,7 +17,7 @@ import logging import sys - +import traceback import bpy from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE, STATE_INITIAL, STATE_LOBBY, STATE_QUITTING, @@ -112,7 +112,8 @@ class ApplyTimer(Timer): try: session.apply(node) except Exception as e: - logging.error(f"Fail to apply {node_ref.uuid}: {e}") + logging.error(f"Fail to apply {node_ref.uuid}") + traceback.print_exc() else: if node_ref.bl_reload_parent: for parent in session._graph.find_parents(node): From 9c633c35ec981123ac7da800ddaa625e8cbaa0d1 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 2 Apr 2021 10:01:45 +0200 Subject: [PATCH 36/66] fix: geometry node socket for blender 2.93 --- multi_user/bl_types/bl_object.py | 43 +++++++++++++++++++------------- multi_user/libs/replication | 1 + 2 files changed, 27 insertions(+), 17 deletions(-) create mode 160000 multi_user/libs/replication diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 3f72d39..80142c4 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -30,13 +30,20 @@ from .dump_anything import ( np_dump_collection) - SKIN_DATA = [ 'radius', 'use_loose', 'use_root' ] +if bpy.app.version[1] >= 93: + SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float) +else: + SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str) + logging.warning("Geometry node Float parameter not supported in \ + blender 2.92.") + + def get_input_index(e): return int(re.findall('[0-9]+', e)[0]) @@ -55,9 +62,7 @@ def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list: dumped_input = None if isinstance(input_value, bpy.types.ID): dumped_input = input_value.uuid - elif isinstance(input_value, float): - logging.warning("Float parameter not supported in blender 2.92, skipping it") - elif isinstance(input_value,(int, str)): + elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS): dumped_input = input_value elif hasattr(input_value, 'to_list'): dumped_input = input_value.to_list() @@ -80,14 +85,12 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b for input_index, input_name in enumerate(inputs_name): dumped_value = dumped_modifier['inputs'][input_index] input_value = target_modifier[input_name] - if isinstance(input_value, float): - logging.warning("Float parameter not supported in blender 2.92, skipping it") - elif isinstance(input_value,(int, str)): + if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS): target_modifier[input_name] = dumped_value elif hasattr(input_value, 'to_list'): for index in range(len(input_value)): input_value[index] = dumped_value[index] - else: + elif input_value and isinstance(input_value, bpy.types.ID): target_modifier[input_name] = get_datablock_from_uuid( dumped_value, None) @@ -179,6 +182,7 @@ def find_geometry_nodes(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types. return nodes_groups + def dump_vertex_groups(src_object: bpy.types.Object) -> dict: """ Dump object's vertex groups @@ -223,6 +227,7 @@ def load_vertex_groups(dumped_vertex_groups: dict, target_object: bpy.types.Obje for index, weight in vg['vertices']: vertex_group.add([index], weight, 'REPLACE') + class BlObject(BlDatablock): bl_id = "objects" bl_class = bpy.types.Object @@ -358,18 +363,21 @@ class BlObject(BlDatablock): SKIN_DATA) if hasattr(target, 'cycles_visibility') \ - and 'cycles_visibility' in data: + and 'cycles_visibility' in data: loader.load(target.cycles_visibility, data['cycles_visibility']) # TODO: handle geometry nodes input from dump_anything if hasattr(target, 'modifiers'): - nodes_modifiers = [mod for mod in target.modifiers if mod.type == 'NODES'] + nodes_modifiers = [ + mod for mod in target.modifiers if mod.type == 'NODES'] for modifier in nodes_modifiers: - load_modifier_geometry_node_inputs(data['modifiers'][modifier.name], modifier) + load_modifier_geometry_node_inputs( + data['modifiers'][modifier.name], modifier) transform = data.get('transforms', None) if transform: - target.matrix_parent_inverse = mathutils.Matrix(transform['matrix_parent_inverse']) + target.matrix_parent_inverse = mathutils.Matrix( + transform['matrix_parent_inverse']) target.matrix_basis = mathutils.Matrix(transform['matrix_basis']) target.matrix_local = mathutils.Matrix(transform['matrix_local']) @@ -435,7 +443,7 @@ class BlObject(BlDatablock): # PARENTING if instance.parent: - data['parent_id'] = instance.parent.name + data['parent_id'] = instance.parent.name # MODIFIERS if hasattr(instance, 'modifiers'): @@ -448,7 +456,8 @@ class BlObject(BlDatablock): data["modifiers"][modifier.name] = dumper.dump(modifier) # hack to dump geometry nodes inputs if modifier.type == 'NODES': - dumped_inputs = dump_modifier_geometry_node_inputs(modifier) + dumped_inputs = dump_modifier_geometry_node_inputs( + modifier) data["modifiers"][modifier.name]['inputs'] = dumped_inputs gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None) @@ -515,7 +524,6 @@ class BlObject(BlDatablock): bone_groups[group.name] = dumper.dump(group) data['pose']['bone_groups'] = bone_groups - # VERTEx GROUP if len(instance.vertex_groups) > 0: data['vertex_groups'] = dump_vertex_groups(instance) @@ -552,7 +560,8 @@ class BlObject(BlDatablock): if hasattr(object_data, 'skin_vertices') and object_data.skin_vertices: skin_vertices = list() for skin_data in object_data.skin_vertices: - skin_vertices.append(np_dump_collection(skin_data.data, SKIN_DATA)) + skin_vertices.append( + np_dump_collection(skin_data.data, SKIN_DATA)) data['skin_vertices'] = skin_vertices # CYCLE SETTINGS @@ -579,7 +588,7 @@ class BlObject(BlDatablock): if self.is_library: deps.append(self.instance.library) - if self.instance.parent : + if self.instance.parent: deps.append(self.instance.parent) if self.instance.instance_type == 'COLLECTION': diff --git a/multi_user/libs/replication b/multi_user/libs/replication new file mode 160000 index 0000000..9a02e16 --- /dev/null +++ b/multi_user/libs/replication @@ -0,0 +1 @@ +Subproject commit 9a02e16d70b03bd3a49722f899ac19dd5d3f8019 From 4e19c169b2684d8d9080cdf0c77fb69881f994fe Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 2 Apr 2021 15:51:31 +0200 Subject: [PATCH 37/66] fix: node_groups unordered socket loading fix: geometry_node sample texture handling fix: geometry node dependencies --- multi_user/bl_types/bl_material.py | 20 +++++++---- multi_user/bl_types/bl_object.py | 55 +++++++++++++++++------------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 3dc82ca..51e4130 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -27,7 +27,7 @@ from .dump_anything import Loader, Dumper from .bl_datablock import BlDatablock, get_datablock_from_uuid NODE_SOCKET_INDEX = re.compile('\[(\d*)\]') -IGNORED_SOCKETS = ['GEOMETRY', 'SHADER'] +IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM'] def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): """ Load a node into a node_tree from a dict @@ -54,8 +54,8 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): if inputs_data: inputs = [i for i in target_node.inputs if i.type not in IGNORED_SOCKETS] for idx, inpt in enumerate(inputs): - loaded_input = inputs_data[idx] if idx < len(inputs_data) and hasattr(inpt, "default_value"): + loaded_input = inputs_data[idx] try: if inpt.type in ['OBJECT', 'COLLECTION']: inpt.default_value = get_datablock_from_uuid(loaded_input, None) @@ -69,13 +69,17 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): outputs_data = node_data.get('outputs') if outputs_data: outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS] - for idx, output in enumerate(outputs_data): - if idx < len(outputs) and hasattr(outputs[idx], "default_value"): + for idx, output in enumerate(outputs): + if idx < len(outputs_data) and hasattr(output, "default_value"): + loaded_output = outputs_data[idx] try: - outputs[idx].default_value = output + if output.type in ['OBJECT', 'COLLECTION']: + output.default_value = get_datablock_from_uuid(loaded_output, None) + else: + output.default_value = loaded_output except Exception as e: logging.warning( - f"Node {target_node.name} output {outputs[idx].name} parameter not supported, skipping ({e})") + f"Node {target_node.name} output {output.name} parameter not supported, skipping ({e})") else: logging.warning( f"Node {target_node.name} output length mismatch.") @@ -328,6 +332,8 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list: def has_node_group(node): return ( hasattr(node, 'node_tree') and node.node_tree) + def has_texture(node): return ( + node.type in ['ATTRIBUTE_SAMPLE_TEXTURE','TEXTURE'] and node.texture) deps = [] for node in node_tree.nodes: @@ -335,6 +341,8 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list: deps.append(node.image) elif has_node_group(node): deps.append(node.node_tree) + elif has_texture(node): + deps.append(node.texture) return deps diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 80142c4..2141159 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -23,6 +23,7 @@ import mathutils from replication.exception import ContextError from .bl_datablock import BlDatablock, get_datablock_from_uuid +from .bl_material import IGNORED_SOCKETS from .dump_anything import ( Dumper, Loader, @@ -43,10 +44,15 @@ else: logging.warning("Geometry node Float parameter not supported in \ blender 2.92.") - -def get_input_index(e): - return int(re.findall('[0-9]+', e)[0]) - +def get_node_group_inputs(node_group): + inputs = [] + for inpt in node_group.inputs: + if inpt.type in IGNORED_SOCKETS: + continue + else: + inputs.append(inpt) + return inputs + # return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS] def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list: """ Dump geometry node modifier input properties @@ -54,11 +60,10 @@ def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list: :arg modifier: geometry node modifier to dump :type modifier: bpy.type.Modifier """ - inputs_name = [p for p in dir(modifier) if "Input_" in p] - inputs_name.sort(key=get_input_index) dumped_inputs = [] - for inputs_index, input_name in enumerate(inputs_name): - input_value = modifier[input_name] + for inpt in get_node_group_inputs(modifier.node_group): + input_value = modifier[inpt.identifier] + dumped_input = None if isinstance(input_value, bpy.types.ID): dumped_input = input_value.uuid @@ -80,18 +85,16 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b :type target_modifier: bpy.type.Modifier """ - inputs_name = [p for p in dir(target_modifier) if "Input_" in p] - inputs_name.sort(key=get_input_index) - for input_index, input_name in enumerate(inputs_name): + for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)): dumped_value = dumped_modifier['inputs'][input_index] - input_value = target_modifier[input_name] + input_value = target_modifier[inpt.identifier] if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS): - target_modifier[input_name] = dumped_value + target_modifier[inpt.identifier] = dumped_value elif hasattr(input_value, 'to_list'): for index in range(len(input_value)): input_value[index] = dumped_value[index] - elif input_value and isinstance(input_value, bpy.types.ID): - target_modifier[input_name] = get_datablock_from_uuid( + elif inpt.type in ['COLLECTION', 'OBJECT']: + target_modifier[inpt.identifier] = get_datablock_from_uuid( dumped_value, None) @@ -168,19 +171,23 @@ def find_textures_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy return textures -def find_geometry_nodes(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]: - """ Find geometry nodes group from a modifier stack +def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]: + """ Find geometry nodes dependencies from a modifier stack :arg modifiers: modifiers collection :type modifiers: bpy.types.bpy_prop_collection :return: list of bpy.types.NodeTree pointers """ - nodes_groups = [] - for item in modifiers: - if item.type == 'NODES' and item.node_group: - nodes_groups.append(item.node_group) - - return nodes_groups + dependencies = [] + for mod in modifiers: + if mod.type == 'NODES' and mod.node_group: + dependencies.append(mod.node_group) + for inpt in get_node_group_inputs(mod.node_group): + parameter = mod.get(inpt.identifier) + if parameter and isinstance(parameter, bpy.types.ID): + dependencies.append(parameter) + logging.info(dependencies) + return dependencies def dump_vertex_groups(src_object: bpy.types.Object) -> dict: @@ -597,6 +604,6 @@ class BlObject(BlDatablock): if self.instance.modifiers: deps.extend(find_textures_dependencies(self.instance.modifiers)) - deps.extend(find_geometry_nodes(self.instance.modifiers)) + deps.extend(find_geometry_nodes_dependencies(self.instance.modifiers)) return deps From 9f167256d01a85c32a181720c8b197fedc7b29c7 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 2 Apr 2021 16:12:51 +0200 Subject: [PATCH 38/66] fix: node frame trasform --- multi_user/bl_types/bl_material.py | 9 +++++++++ multi_user/bl_types/bl_object.py | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 51e4130..1b71058 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -123,6 +123,9 @@ def dump_node(node: bpy.types.ShaderNode) -> dict: dumped_node = node_dumper.dump(node) + if node.parent: + dumped_node['parent'] = node.parent.name + dump_io_needed = (node.type not in ['REROUTE', 'OUTPUT_MATERIAL']) if dump_io_needed: @@ -318,6 +321,12 @@ def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeT for node in node_tree_data["nodes"]: load_node(node_tree_data["nodes"][node], target_node_tree) + for node_id, node_data in node_tree_data["nodes"].items(): + target_node = target_node_tree.nodes[node_id] + if 'parent' in node_data: + target_node.parent = target_node_tree.nodes[node_data['parent']] + else: + target_node.parent = None # TODO: load only required nodes links # Load nodes links target_node_tree.links.clear() diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 2141159..9a1df18 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -182,10 +182,10 @@ def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) - for mod in modifiers: if mod.type == 'NODES' and mod.node_group: dependencies.append(mod.node_group) - for inpt in get_node_group_inputs(mod.node_group): - parameter = mod.get(inpt.identifier) - if parameter and isinstance(parameter, bpy.types.ID): - dependencies.append(parameter) + # for inpt in get_node_group_inputs(mod.node_group): + # parameter = mod.get(inpt.identifier) + # if parameter and isinstance(parameter, bpy.types.ID): + # dependencies.append(parameter) logging.info(dependencies) return dependencies From 5e30e215ab21cfb65f9ce6daf0238fd54cf51327 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 2 Apr 2021 16:37:47 +0200 Subject: [PATCH 39/66] fix: empty node --- multi_user/bl_types/bl_material.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 1b71058..f6fec50 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -322,8 +322,10 @@ def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeT load_node(node_tree_data["nodes"][node], target_node_tree) for node_id, node_data in node_tree_data["nodes"].items(): - target_node = target_node_tree.nodes[node_id] - if 'parent' in node_data: + target_node = target_node_tree.nodes.get(node_id, None) + if target_node is None: + continue + elif 'parent' in node_data: target_node.parent = target_node_tree.nodes[node_data['parent']] else: target_node.parent = None From cb85a1db4c94401f2bd22cc58e7a9c2ddc52ba65 Mon Sep 17 00:00:00 2001 From: Swann Date: Tue, 13 Apr 2021 14:37:43 +0200 Subject: [PATCH 40/66] feat: dual identification for object parents --- multi_user/bl_types/bl_object.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 9a1df18..a5e3bb6 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -317,9 +317,9 @@ class BlObject(BlDatablock): loader.load(target.display, data['display']) # Parenting - parent_id = data.get('parent_id') + parent_id = data.get('parent_uid') if parent_id: - parent = bpy.data.objects[parent_id] + parent = get_datablock_from_uuid(parent_id[0], bpy.data.objects[parent_id[1]]) # Avoid reloading if target.parent != parent and parent is not None: target.parent = parent @@ -450,7 +450,7 @@ class BlObject(BlDatablock): # PARENTING if instance.parent: - data['parent_id'] = instance.parent.name + data['parent_uid'] = (instance.parent.uuid, instance.parent.name) # MODIFIERS if hasattr(instance, 'modifiers'): From 826a59085edf7ac77d3d1478be4d27a2aada2115 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 09:45:18 +0200 Subject: [PATCH 41/66] feat: particle texture slot support --- multi_user/bl_types/bl_particle.py | 37 +++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/multi_user/bl_types/bl_particle.py b/multi_user/bl_types/bl_particle.py index db31562..b97efcb 100644 --- a/multi_user/bl_types/bl_particle.py +++ b/multi_user/bl_types/bl_particle.py @@ -4,7 +4,11 @@ import mathutils from . import dump_anything from .bl_datablock import BlDatablock, get_datablock_from_uuid -def dump_textures_slots(texture_slots): + +def dump_textures_slots(texture_slots: bpy.types.bpy_prop_collection) -> list: + """ Dump every texture slot collection as the form: + [(index, slot_texture_uuid, slot_texture_name), (), ...] + """ dumped_slots = [] for index, slot in enumerate(texture_slots): if slot and slot.texture: @@ -12,12 +16,19 @@ def dump_textures_slots(texture_slots): return dumped_slots -def load_texture_slots(dumped_slots, target_slots): + +def load_texture_slots(dumped_slots: list, target_slots: bpy.types.bpy_prop_collection): + """ + """ for index, slot in enumerate(target_slots): - target_slots.clear(index) + if slot: + target_slots.clear(index) for index, slot_uuid, slot_name in dumped_slots: - target_slots.create(index).texture = get_datablock_from_uuid(slot_uuid, slot_name) + target_slots.create(index).texture = get_datablock_from_uuid( + slot_uuid, slot_name + ) + class BlParticle(BlDatablock): bl_id = "particles" @@ -32,19 +43,19 @@ class BlParticle(BlDatablock): def _load_implementation(self, data, target): dump_anything.load(target, data) - dump_anything.load(target.effector_weights, data['effector_weights']) + dump_anything.load(target.effector_weights, data["effector_weights"]) # Force field - force_field_1 = data.get('force_field_1', None) + force_field_1 = data.get("force_field_1", None) if force_field_1: dump_anything.load(target.force_field_1, force_field_1) - force_field_2 = data.get('force_field_2', None) + force_field_2 = data.get("force_field_2", None) if force_field_2: dump_anything.load(target.force_field_2, force_field_2) # Texture slots - # load_texture_slots(data['texture_slots'], target.texture_slots) + load_texture_slots(data["texture_slots"], target.texture_slots) def _dump_implementation(self, data, instance=None): assert instance @@ -54,16 +65,16 @@ class BlParticle(BlDatablock): data = dumper.dump(instance) # Particle effectors - data['effector_weights'] = dumper.dump(instance.effector_weights) + data["effector_weights"] = dumper.dump(instance.effector_weights) if instance.force_field_1: - data['force_field_1'] = dumper.dump(instance.force_field_1) + data["force_field_1"] = dumper.dump(instance.force_field_1) if instance.force_field_2: - data['force_field_2'] = dumper.dump(instance.force_field_2) + data["force_field_2"] = dumper.dump(instance.force_field_2) # Texture slots - # data['texture_slots'] = dump_textures_slots(instance.texture_slots) + data["texture_slots"] = dump_textures_slots(instance.texture_slots) return data def _resolve_deps_implementation(self): - return [ t.texture for t in self.instance.texture_slots if t] \ No newline at end of file + return [t.texture for t in self.instance.texture_slots if t and t.texture] From 12acd226606103ceefffeff92f99de29dee24406 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 09:54:34 +0200 Subject: [PATCH 42/66] feat: ignore some attributes --- multi_user/bl_types/bl_object.py | 5 +++++ multi_user/bl_types/bl_particle.py | 8 ++++++++ multi_user/libs/replication | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 95e37b5..8ab4f82 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -479,6 +479,11 @@ class BlObject(BlDatablock): dumped_modifier['inputs'] = dumped_inputs if modifier.type == 'PARTICLE_SYSTEM': + dumper.exclude_filter = [ + "is_edited", + "is_editable", + "is_global_hair" + ] dumped_modifier['particle_system'] = dumper.dump(modifier.particle_system) data["modifiers"][modifier.name] = dumped_modifier diff --git a/multi_user/bl_types/bl_particle.py b/multi_user/bl_types/bl_particle.py index b97efcb..b7d7697 100644 --- a/multi_user/bl_types/bl_particle.py +++ b/multi_user/bl_types/bl_particle.py @@ -29,6 +29,13 @@ def load_texture_slots(dumped_slots: list, target_slots: bpy.types.bpy_prop_coll slot_uuid, slot_name ) +IGNORED_ATTR = [ + "is_embedded_data", + "is_evaluated", + "is_fluid", + "is_library_indirect", + "users" +] class BlParticle(BlDatablock): bl_id = "particles" @@ -62,6 +69,7 @@ class BlParticle(BlDatablock): dumper = dump_anything.Dumper() dumper.depth = 1 + dumper.exclude_filter = IGNORED_ATTR data = dumper.dump(instance) # Particle effectors diff --git a/multi_user/libs/replication b/multi_user/libs/replication index 9a02e16..0614a09 160000 --- a/multi_user/libs/replication +++ b/multi_user/libs/replication @@ -1 +1 @@ -Subproject commit 9a02e16d70b03bd3a49722f899ac19dd5d3f8019 +Subproject commit 0614a09e70cb52e57c4f3bb1b9e02876f8b6c6cd From d9d5a34653006d5fc644a6011ce95e1595e41c1c Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 09:56:07 +0200 Subject: [PATCH 43/66] clean: remove libs --- multi_user/libs/replication | 1 - 1 file changed, 1 deletion(-) delete mode 160000 multi_user/libs/replication diff --git a/multi_user/libs/replication b/multi_user/libs/replication deleted file mode 160000 index 0614a09..0000000 --- a/multi_user/libs/replication +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0614a09e70cb52e57c4f3bb1b9e02876f8b6c6cd From 552c649d342571a56945f4ba466e3e8544a49db5 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 11:49:34 +0200 Subject: [PATCH 44/66] feat: physics forcefield and collision support --- multi_user/bl_types/bl_object.py | 93 ++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 8ab4f82..dc84433 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -54,6 +54,93 @@ def get_node_group_inputs(node_group): return inputs # return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS] +def get_rigid_body_scenes(target: bpy.types.Object)->[str]: + """ Find the list of scene using the rigid body + """ + scenes = [] + for scene in bpy.data.scenes: + if scene.rigidbody_world: + for obj in scene.rigidbody_world.collection.objects: + if obj == target: + scenes.append((scene.name, scene.rigidbody_world.collection.name)) + break + return scenes + +def dump_physics(target: bpy.types.Object)->dict: + """ + Dump all physics settings from a given object excluding modifier + related physics settings (such as softbody, cloth, dynapaint and fluid) + """ + dumper = Dumper() + dumper.depth = 1 + physics_data = {} + + # Collisions (collision) + if target.collision and target.collision.use: + physics_data['collision'] = dumper.dump(target.collision) + + # Field (field) + if target.field and target.field.type != "None": + physics_data['field'] = dumper.dump(target.field) + + # Rigid Body (rigid_body) + if target.rigid_body: + logging.warning("Rigid body not synced yet. ") + # physics_data['rigid_body_scenes'] = get_rigid_body_scenes(target) + # physics_data['rigid_body'] = dumper.dump(target.rigid_body) + + # Rigid Body constraint (rigid_body_constraint) + if target.rigid_body_constraint: + logging.warning("Rigid body constraints not synced yet. ") + # physics_data['rigid_body_constraint'] = dumper.dump(target.rigid_body_constraint) + + return physics_data + +def load_physics(dumped_settings: dict, target: bpy.types.Object): + """ Load all physics settings from a given object excluding modifier + related physics settings (such as softbody, cloth, dynapaint and fluid) + """ + loader = Loader() + + if 'collision' in dumped_settings: + loader.load(target.collision, dumped_settings['collision']) + + if 'field' in dumped_settings: + loader.load(target.field, dumped_settings['field']) + + # if 'rigid_body' in dumped_settings: + # # ugly fix to link the rigid body to the scene + # for scene, physics_collection in dumped_settings['rigid_body_scenes']: + # scene = bpy.data.scenes[scene] + # phys_collection = bpy.data.collections.get(physics_collection) + + # # Create the scene physics settings + # if not scene.rigidbody_world: + # ctx_override = {'scene': scene} + # bpy.ops.rigidbody.world_add(ctx_override) + + # if not phys_collection: + # phys_collection = bpy.data.collections.new(physics_collection) + + # if not scene.rigidbody_world.collection \ + # or scene.rigidbody_world.collection != phys_collection : + # scene.rigidbody_world.collection = phys_collection + + # if target.name not in phys_collection.objects: + # phys_collection.objects.link(target) + + # loader.load(target.rigid_body, dumped_settings['rigid_body']) + # elif target.rigid_body: + # scenes = get_rigid_body_scenes(target) + # for scene, collection in scenes: + # phys_collection = bpy.data.collections.get(collection) + # if phys_collection and target.name in phys_collection.objects: + # phys_collection.objects.unlink(target) + + # if 'rigid_body_constraint' in dumped_settings: + # loader.load(target.rigid_body_constraint, dumped_settings['rigid_body_constraint']) + + def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list: """ Dump geometry node modifier input properties @@ -399,6 +486,9 @@ class BlObject(BlDatablock): target.matrix_basis = mathutils.Matrix(transform['matrix_basis']) target.matrix_local = mathutils.Matrix(transform['matrix_local']) + # PHYSICS + load_physics(data, target) + def _dump_implementation(self, data, instance=None): assert(instance) @@ -606,6 +696,9 @@ class BlObject(BlDatablock): ] data['cycles_visibility'] = dumper.dump(instance.cycles_visibility) + # PHYSICS + data.update(dump_physics(instance)) + return data def _resolve_deps_implementation(self): From 70641435ccc501635d34696e717dab7bfc6e24d3 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 12:25:16 +0200 Subject: [PATCH 45/66] feat: initial rigid body supports --- multi_user/bl_types/bl_object.py | 67 +++++++++----------------------- 1 file changed, 18 insertions(+), 49 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index dc84433..e625cac 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -54,17 +54,6 @@ def get_node_group_inputs(node_group): return inputs # return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS] -def get_rigid_body_scenes(target: bpy.types.Object)->[str]: - """ Find the list of scene using the rigid body - """ - scenes = [] - for scene in bpy.data.scenes: - if scene.rigidbody_world: - for obj in scene.rigidbody_world.collection.objects: - if obj == target: - scenes.append((scene.name, scene.rigidbody_world.collection.name)) - break - return scenes def dump_physics(target: bpy.types.Object)->dict: """ @@ -85,14 +74,11 @@ def dump_physics(target: bpy.types.Object)->dict: # Rigid Body (rigid_body) if target.rigid_body: - logging.warning("Rigid body not synced yet. ") - # physics_data['rigid_body_scenes'] = get_rigid_body_scenes(target) - # physics_data['rigid_body'] = dumper.dump(target.rigid_body) + physics_data['rigid_body'] = dumper.dump(target.rigid_body) # Rigid Body constraint (rigid_body_constraint) if target.rigid_body_constraint: - logging.warning("Rigid body constraints not synced yet. ") - # physics_data['rigid_body_constraint'] = dumper.dump(target.rigid_body_constraint) + physics_data['rigid_body_constraint'] = dumper.dump(target.rigid_body_constraint) return physics_data @@ -108,38 +94,19 @@ def load_physics(dumped_settings: dict, target: bpy.types.Object): if 'field' in dumped_settings: loader.load(target.field, dumped_settings['field']) - # if 'rigid_body' in dumped_settings: - # # ugly fix to link the rigid body to the scene - # for scene, physics_collection in dumped_settings['rigid_body_scenes']: - # scene = bpy.data.scenes[scene] - # phys_collection = bpy.data.collections.get(physics_collection) - - # # Create the scene physics settings - # if not scene.rigidbody_world: - # ctx_override = {'scene': scene} - # bpy.ops.rigidbody.world_add(ctx_override) - - # if not phys_collection: - # phys_collection = bpy.data.collections.new(physics_collection) - - # if not scene.rigidbody_world.collection \ - # or scene.rigidbody_world.collection != phys_collection : - # scene.rigidbody_world.collection = phys_collection - - # if target.name not in phys_collection.objects: - # phys_collection.objects.link(target) - - # loader.load(target.rigid_body, dumped_settings['rigid_body']) - # elif target.rigid_body: - # scenes = get_rigid_body_scenes(target) - # for scene, collection in scenes: - # phys_collection = bpy.data.collections.get(collection) - # if phys_collection and target.name in phys_collection.objects: - # phys_collection.objects.unlink(target) - - # if 'rigid_body_constraint' in dumped_settings: - # loader.load(target.rigid_body_constraint, dumped_settings['rigid_body_constraint']) + if 'rigid_body' in dumped_settings: + if not target.rigid_body: + bpy.ops.rigidbody.object_add({"object": target}) + loader.load(target.rigid_body, dumped_settings['rigid_body']) + elif target.rigid_body: + bpy.ops.rigidbody.object_remove({"object": target}) + if 'rigid_body_constraint' in dumped_settings: + if not target.rigid_body_constraint: + bpy.ops.rigidbody.constraint_add({"object": target}) + loader.load(target.rigid_body_constraint, dumped_settings['rigid_body_constraint']) + elif target.rigid_body_constraint: + bpy.ops.rigidbody.constraint_remove({"object": target}) def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list: """ Dump geometry node modifier input properties @@ -479,6 +446,9 @@ class BlObject(BlDatablock): # Hack to remove the default generated particle settings # bpy.data.particles.remove(default_settings) + # PHYSICS + load_physics(data, target) + transform = data.get('transforms', None) if transform: target.matrix_parent_inverse = mathutils.Matrix( @@ -486,8 +456,7 @@ class BlObject(BlDatablock): target.matrix_basis = mathutils.Matrix(transform['matrix_basis']) target.matrix_local = mathutils.Matrix(transform['matrix_local']) - # PHYSICS - load_physics(data, target) + def _dump_implementation(self, data, instance=None): assert(instance) From eb631e2d4b553b0e523e8a2d16aa845d5ad55415 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 14:36:06 +0200 Subject: [PATCH 46/66] feat: update changelog 0.3.0 release --- CHANGELOG.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 293e81d..49f0192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,4 +157,33 @@ All notable changes to this project will be documented in this file. - Empty and Light object selection highlights - Material renaming - Default material nodes input parameters -- blender 2.91 python api compatibility \ No newline at end of file +- blender 2.91 python api compatibility + +## [0.3.0] - 2021-04-14 + +### Added + +- Curve material support +- Cycle visibility settings +- Session save/load operator +- Add new scene support +- Physic initial support +- Geometry node initial support +- Blender 2.93 compatibility +### Changed + +- Host documentation on Gitlab Page +- Event driven update (from the blender deps graph) + +### Fixed + +- Vertex group assignation +- Parent relation can't be removed +- Separate object +- Delete animation +- Sync missing holdout option for grease pencil material +- Sync missing `skin_vertices` +- Exception access violation during Undo/Redo +- Sync missing armature bone Roll +- Sync missing driver data_path +- Constraint replication \ No newline at end of file From 8e606068f3e201acb711a1c3c806608d8a58eb97 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 15:29:02 +0200 Subject: [PATCH 47/66] fix: particle system duplication feat: update Readme --- README.md | 60 ++++++++++++++-------------- multi_user/bl_types/bl_object.py | 31 ++++++++++---- multi_user/bl_types/bl_particle.py | 4 +- multi_user/bl_types/dump_anything.py | 4 +- 4 files changed, 58 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index d2c8991..051ef32 100644 --- a/README.md +++ b/README.md @@ -29,35 +29,35 @@ See the [troubleshooting guide](https://slumber.gitlab.io/multi-user/getting_sta Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones. -| Name | Status | Comment | -| -------------- | :----: | :--------------------------------------------------------------------------: | -| action | ✔️ | | -| armature | ❗ | Not stable | -| camera | ✔️ | | -| collection | ✔️ | | -| curve | ❗ | Nurbs surfaces not supported | -| gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) | -| image | ✔️ | | -| mesh | ✔️ | | -| material | ✔️ | | -| node_groups | ❗ | Material only | -| geometry nodes | ✔️ | | -| metaball | ✔️ | | -| object | ✔️ | | -| textures | ❗ | Supported for modifiers/materials only | -| texts | ✔️ | | -| scene | ✔️ | | -| world | ✔️ | | -| lightprobes | ✔️ | | -| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) | -| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) | -| nla | ❌ | | -| 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 | -| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) | -| libraries | ❗ | Partial | +| Name | Status | Comment | +| -------------- | :----: | :----------------------------------------------------------: | +| action | ✔️ | | +| armature | ❗ | Not stable | +| camera | ✔️ | | +| collection | ✔️ | | +| curve | ❗ | Nurbs surfaces not supported | +| gpencil | ✔️ | | +| image | ✔️ | | +| mesh | ✔️ | | +| material | ✔️ | | +| node_groups | ❗ | Material & Geometry only | +| geometry nodes | ✔️ | | +| metaball | ✔️ | | +| object | ✔️ | | +| textures | ❗ | Supported for modifiers/materials/geo nodes only | +| texts | ✔️ | | +| scene | ✔️ | | +| world | ✔️ | | +| lightprobes | ✔️ | | +| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) | +| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) | +| nla | ❌ | | +| volumes | ✔️ | | +| particles | ❗ | The cache isn't syncing. | +| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) | +| vse | ❗ | Mask and Clip not supported yet | +| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) | +| libraries | ❗ | Partial | @@ -70,7 +70,7 @@ I'm working on it. | Dependencies | Version | Needed | | ------------ | :-----: | -----: | -| Replication | latest | yes | +| Replication | latest | yes | diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index e625cac..fd9bbd5 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -439,12 +439,24 @@ class BlObject(BlDatablock): mod for mod in target.modifiers if mod.type == 'PARTICLE_SYSTEM'] for mod in particles_modifiers: - loader.load(mod.particle_system, data['modifiers'][mod.name]['particle_system']) - # default_settings = mod.particle_system.settings - # mod.particle_system.settings = get_datablock_from_uuid(data['modifiers'][mod.name]['particle_system']['settings'], None) + default = mod.particle_system.settings.name + dumped_particles = data['modifiers'][mod.name]['particle_system'] + loader.load(mod.particle_system, dumped_particles) - # Hack to remove the default generated particle settings - # bpy.data.particles.remove(default_settings) + settings = get_datablock_from_uuid(dumped_particles['settings_uuid'], None) + if settings: + mod.particle_system.settings = settings + + # Hack to remove the default generated particle settings + for settings in bpy.data.particles: + if settings.users == 0: + bpy.data.particles.remove(settings) + + phys_modifiers = [ + mod for mod in target.modifiers if mod.type in ['SOFT_BODY', 'CLOTH']] + + for mod in phys_modifiers: + loader.load(mod.settings, data['modifiers'][mod.name]['settings']) # PHYSICS load_physics(data, target) @@ -456,7 +468,6 @@ class BlObject(BlDatablock): target.matrix_basis = mathutils.Matrix(transform['matrix_basis']) target.matrix_local = mathutils.Matrix(transform['matrix_local']) - def _dump_implementation(self, data, instance=None): assert(instance) @@ -537,13 +548,17 @@ class BlObject(BlDatablock): modifier) dumped_modifier['inputs'] = dumped_inputs - if modifier.type == 'PARTICLE_SYSTEM': + elif modifier.type == 'PARTICLE_SYSTEM': dumper.exclude_filter = [ "is_edited", "is_editable", "is_global_hair" ] dumped_modifier['particle_system'] = dumper.dump(modifier.particle_system) + dumped_modifier['particle_system']['settings_uuid'] = modifier.particle_system.settings.uuid + + elif modifier.type in ['SOFT_BODY', 'CLOTH']: + dumped_modifier['settings'] = dumper.dump(modifier.settings) data["modifiers"][modifier.name] = dumped_modifier @@ -679,7 +694,7 @@ class BlObject(BlDatablock): # Particle systems for particle_slot in self.instance.particle_systems: - deps.append(bpy.data.particles[particle_slot.name]) + deps.append(particle_slot.settings) if self.is_library: deps.append(self.instance.library) diff --git a/multi_user/bl_types/bl_particle.py b/multi_user/bl_types/bl_particle.py index b7d7697..2ec6fac 100644 --- a/multi_user/bl_types/bl_particle.py +++ b/multi_user/bl_types/bl_particle.py @@ -45,7 +45,9 @@ class BlParticle(BlDatablock): bl_reload_parent = False def _construct(self, data): - return bpy.data.particles.new(data["name"]) + instance = bpy.data.particles.new(data["name"]) + instance.uuid = self.uuid + return instance def _load_implementation(self, data, target): dump_anything.load(target, data) diff --git a/multi_user/bl_types/dump_anything.py b/multi_user/bl_types/dump_anything.py index 10e1293..2b97ecb 100644 --- a/multi_user/bl_types/dump_anything.py +++ b/multi_user/bl_types/dump_anything.py @@ -610,8 +610,8 @@ class Loader: instance.write(bpy.data.fonts.get(dump)) elif isinstance(rna_property_type, T.Sound): instance.write(bpy.data.sounds.get(dump)) - elif isinstance(rna_property_type, T.ParticleSettings): - instance.write(bpy.data.particles.get(dump)) + # elif isinstance(rna_property_type, T.ParticleSettings): + # instance.write(bpy.data.particles.get(dump)) def _load_matrix(self, matrix, dump): matrix.write(mathutils.Matrix(dump)) From 736c3df7c4a452d5ec2b6b7486dbdbb74431336d Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 15:50:53 +0200 Subject: [PATCH 48/66] feat: remove new particle systems clean: remove logs --- multi_user/bl_types/bl_object.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index fd9bbd5..18945cb 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -240,7 +240,7 @@ def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) - # parameter = mod.get(inpt.identifier) # if parameter and isinstance(parameter, bpy.types.ID): # dependencies.append(parameter) - logging.info(dependencies) + return dependencies @@ -439,18 +439,16 @@ class BlObject(BlDatablock): mod for mod in target.modifiers if mod.type == 'PARTICLE_SYSTEM'] for mod in particles_modifiers: - default = mod.particle_system.settings.name + default = mod.particle_system.settings dumped_particles = data['modifiers'][mod.name]['particle_system'] loader.load(mod.particle_system, dumped_particles) settings = get_datablock_from_uuid(dumped_particles['settings_uuid'], None) if settings: mod.particle_system.settings = settings - - # Hack to remove the default generated particle settings - for settings in bpy.data.particles: - if settings.users == 0: - bpy.data.particles.remove(settings) + # Hack to remove the default generated particle settings + if not default.uuid: + bpy.data.particles.remove(default) phys_modifiers = [ mod for mod in target.modifiers if mod.type in ['SOFT_BODY', 'CLOTH']] From 570909a7c471d6c33cf063e3b1dec5a4ba9a85e3 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 14 Apr 2021 16:25:21 +0200 Subject: [PATCH 49/66] fix: prevent field from being dumped if unused fix: bl_object tests --- multi_user/bl_types/bl_object.py | 3 ++- scripts/test_addon.py | 2 +- tests/test_bl_types/test_object.py | 7 +++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 18945cb..0220646 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -69,7 +69,7 @@ def dump_physics(target: bpy.types.Object)->dict: physics_data['collision'] = dumper.dump(target.collision) # Field (field) - if target.field and target.field.type != "None": + if target.field and target.field.type != "NONE": physics_data['field'] = dumper.dump(target.field) # Rigid Body (rigid_body) @@ -538,6 +538,7 @@ class BlObject(BlDatablock): if modifiers: dumper.include_filter = None dumper.depth = 1 + dumper.exclude_filter = ['is_active'] for index, modifier in enumerate(modifiers): dumped_modifier = dumper.dump(modifier) # hack to dump geometry nodes inputs diff --git a/scripts/test_addon.py b/scripts/test_addon.py index 2f524dd..96575a9 100644 --- a/scripts/test_addon.py +++ b/scripts/test_addon.py @@ -13,7 +13,7 @@ def main(): if len(sys.argv) > 2: blender_rev = sys.argv[2] else: - blender_rev = "2.91.0" + blender_rev = "2.92.0" try: exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev) diff --git a/tests/test_bl_types/test_object.py b/tests/test_bl_types/test_object.py index f73b848..db63981 100644 --- a/tests/test_bl_types/test_object.py +++ b/tests/test_bl_types/test_object.py @@ -7,7 +7,7 @@ import bpy import random from multi_user.bl_types.bl_object import BlObject -# Removed 'BUILD' modifier because the seed doesn't seems to be +# Removed 'BUILD', 'SOFT_BODY' modifier because the seed doesn't seems to be # correctly initialized (#TODO: report the bug) MOFIFIERS_TYPES = [ 'DATA_TRANSFER', 'MESH_CACHE', 'MESH_SEQUENCE_CACHE', @@ -22,8 +22,7 @@ MOFIFIERS_TYPES = [ 'MESH_DEFORM', 'SHRINKWRAP', 'SIMPLE_DEFORM', 'SMOOTH', 'CORRECTIVE_SMOOTH', 'LAPLACIANSMOOTH', 'SURFACE_DEFORM', 'WARP', 'WAVE', 'CLOTH', 'COLLISION', 'DYNAMIC_PAINT', - 'EXPLODE', 'FLUID', 'OCEAN', 'PARTICLE_INSTANCE', - 'SOFT_BODY', 'SURFACE'] + 'EXPLODE', 'FLUID', 'OCEAN', 'PARTICLE_INSTANCE', 'SURFACE'] GP_MODIFIERS_TYPE = [ 'GP_ARRAY', 'GP_BUILD', 'GP_MIRROR', 'GP_MULTIPLY', @@ -72,5 +71,5 @@ def test_object(clear_blend): test = implementation._construct(expected) implementation._load(expected, test) result = implementation._dump(test) - + print(DeepDiff(expected, result)) assert not DeepDiff(expected, result) From 53eaaa2fcd768b236c800c82976e237ebcf15b98 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 15 Apr 2021 15:28:59 +0200 Subject: [PATCH 50/66] fix: auto-updater operator registration for blender 2.93 compatibility --- multi_user/addon_updater_ops.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/multi_user/addon_updater_ops.py b/multi_user/addon_updater_ops.py index 9dd3960..efe7641 100644 --- a/multi_user/addon_updater_ops.py +++ b/multi_user/addon_updater_ops.py @@ -122,13 +122,13 @@ class addon_updater_install_popup(bpy.types.Operator): # if true, run clean install - ie remove all files before adding new # equivalent to deleting the addon and reinstalling, except the # updater folder/backup folder remains - clean_install = bpy.props.BoolProperty( + clean_install: bpy.props.BoolProperty( name="Clean install", description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", default=False, options={'HIDDEN'} ) - ignore_enum = bpy.props.EnumProperty( + ignore_enum: bpy.props.EnumProperty( name="Process update", description="Decide to install, ignore, or defer new addon update", items=[ @@ -264,7 +264,7 @@ class addon_updater_update_now(bpy.types.Operator): # if true, run clean install - ie remove all files before adding new # equivalent to deleting the addon and reinstalling, except the # updater folder/backup folder remains - clean_install = bpy.props.BoolProperty( + clean_install: bpy.props.BoolProperty( name="Clean install", description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", default=False, @@ -332,7 +332,7 @@ class addon_updater_update_target(bpy.types.Operator): i+=1 return ret - target = bpy.props.EnumProperty( + target: bpy.props.EnumProperty( name="Target version to install", description="Select the version to install", items=target_version @@ -341,7 +341,7 @@ class addon_updater_update_target(bpy.types.Operator): # if true, run clean install - ie remove all files before adding new # equivalent to deleting the addon and reinstalling, except the # updater folder/backup folder remains - clean_install = bpy.props.BoolProperty( + clean_install: bpy.props.BoolProperty( name="Clean install", description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", default=False, @@ -399,7 +399,7 @@ class addon_updater_install_manually(bpy.types.Operator): bl_description = "Proceed to manually install update" bl_options = {'REGISTER', 'INTERNAL'} - error = bpy.props.StringProperty( + error: bpy.props.StringProperty( name="Error Occurred", default="", options={'HIDDEN'} @@ -461,7 +461,7 @@ class addon_updater_updated_successful(bpy.types.Operator): bl_description = "Update installation response" bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} - error = bpy.props.StringProperty( + error: bpy.props.StringProperty( name="Error Occurred", default="", options={'HIDDEN'} From fb61b380b6321b48cc61494156cde10c1023f200 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 22 Apr 2021 11:05:34 +0200 Subject: [PATCH 51/66] fix: uv_projector modifier refactor: move modifier related code to dump_modifiers and load_modifier_custom_data --- multi_user/bl_types/bl_object.py | 130 ++++++++++++++++++------------- multi_user/libs/replication | 1 + 2 files changed, 77 insertions(+), 54 deletions(-) create mode 160000 multi_user/libs/replication diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 0220646..e6a8648 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -289,6 +289,79 @@ def load_vertex_groups(dumped_vertex_groups: dict, target_object: bpy.types.Obje vertex_group.add([index], weight, 'REPLACE') +def dump_modifiers(modifiers: bpy.types.bpy_prop_collection)->dict: + """ Dump all modifiers of a modifier collection into a dict + + :param modifiers: modifiers + :type modifiers: bpy.types.bpy_prop_collection + :return: dict + """ + dumped_modifiers = {} + dumper = Dumper() + dumper.depth = 1 + dumper.exclude_filter = ['is_active'] + + for index, modifier in enumerate(modifiers): + dumped_modifier = dumper.dump(modifier) + # hack to dump geometry nodes inputs + if modifier.type == 'NODES': + dumped_inputs = dump_modifier_geometry_node_inputs( + modifier) + dumped_modifier['inputs'] = dumped_inputs + + elif modifier.type == 'PARTICLE_SYSTEM': + dumper.exclude_filter = [ + "is_edited", + "is_editable", + "is_global_hair" + ] + dumped_modifier['particle_system'] = dumper.dump(modifier.particle_system) + dumped_modifier['particle_system']['settings_uuid'] = modifier.particle_system.settings.uuid + + elif modifier.type in ['SOFT_BODY', 'CLOTH']: + dumped_modifier['settings'] = dumper.dump(modifier.settings) + elif modifier.type == 'UV_PROJECT': + dumped_modifier['projectors'] =[p.object.name for p in modifier.projectors if p and p.object] + + dumped_modifiers[modifier.name] = dumped_modifier + return dumped_modifiers + + +def load_modifiers_custom_data(dumped_modifiers: dict, modifiers: bpy.types.bpy_prop_collection): + """ Load modifiers custom data not managed by the dump_anything loader + + :param dumped_modifiers: modifiers to load + :type dumped_modifiers: dict + :param modifiers: target modifiers collection + :type modifiers: bpy.types.bpy_prop_collection + """ + loader = Loader() + + for modifier in modifiers: + dumped_modifier = dumped_modifiers.get(modifier.name) + if modifier.type == 'NODES': + load_modifier_geometry_node_inputs(dumped_modifier, modifier) + elif modifier.type == 'PARTICLE_SYSTEM': + default = modifier.particle_system.settings + dumped_particles = dumped_modifier['particle_system'] + loader.load(modifier.particle_system, dumped_particles) + + settings = get_datablock_from_uuid(dumped_particles['settings_uuid'], None) + if settings: + modifier.particle_system.settings = settings + # Hack to remove the default generated particle settings + if not default.uuid: + bpy.data.particles.remove(default) + elif modifier.type in ['SOFT_BODY', 'CLOTH']: + loader.load(modifier.settings, dumped_modifier['settings']) + elif modifier.type == 'UV_PROJECT': + for projector_index, projector_object in enumerate(dumped_modifier['projectors']): + target_object = bpy.data.objects.get(projector_object) + if target_object: + modifier.projectors[projector_index].object = target_object + else: + logging.error("Could't load projector target object {projector_object}") + class BlObject(BlDatablock): bl_id = "objects" bl_class = bpy.types.Object @@ -427,34 +500,8 @@ class BlObject(BlDatablock): and 'cycles_visibility' in data: loader.load(target.cycles_visibility, data['cycles_visibility']) - # TODO: handle geometry nodes input from dump_anything if hasattr(target, 'modifiers'): - nodes_modifiers = [ - mod for mod in target.modifiers if mod.type == 'NODES'] - for modifier in nodes_modifiers: - load_modifier_geometry_node_inputs( - data['modifiers'][modifier.name], modifier) - - particles_modifiers = [ - mod for mod in target.modifiers if mod.type == 'PARTICLE_SYSTEM'] - - for mod in particles_modifiers: - default = mod.particle_system.settings - dumped_particles = data['modifiers'][mod.name]['particle_system'] - loader.load(mod.particle_system, dumped_particles) - - settings = get_datablock_from_uuid(dumped_particles['settings_uuid'], None) - if settings: - mod.particle_system.settings = settings - # Hack to remove the default generated particle settings - if not default.uuid: - bpy.data.particles.remove(default) - - phys_modifiers = [ - mod for mod in target.modifiers if mod.type in ['SOFT_BODY', 'CLOTH']] - - for mod in phys_modifiers: - loader.load(mod.settings, data['modifiers'][mod.name]['settings']) + load_modifiers_custom_data(data['modifiers'], target.modifiers) # PHYSICS load_physics(data, target) @@ -532,34 +579,9 @@ class BlObject(BlDatablock): data['parent_uid'] = (instance.parent.uuid, instance.parent.name) # MODIFIERS + modifiers = getattr(instance, 'modifiers', None) if hasattr(instance, 'modifiers'): - data["modifiers"] = {} - modifiers = getattr(instance, 'modifiers', None) - if modifiers: - dumper.include_filter = None - dumper.depth = 1 - dumper.exclude_filter = ['is_active'] - for index, modifier in enumerate(modifiers): - dumped_modifier = dumper.dump(modifier) - # hack to dump geometry nodes inputs - if modifier.type == 'NODES': - dumped_inputs = dump_modifier_geometry_node_inputs( - modifier) - dumped_modifier['inputs'] = dumped_inputs - - elif modifier.type == 'PARTICLE_SYSTEM': - dumper.exclude_filter = [ - "is_edited", - "is_editable", - "is_global_hair" - ] - dumped_modifier['particle_system'] = dumper.dump(modifier.particle_system) - dumped_modifier['particle_system']['settings_uuid'] = modifier.particle_system.settings.uuid - - elif modifier.type in ['SOFT_BODY', 'CLOTH']: - dumped_modifier['settings'] = dumper.dump(modifier.settings) - - data["modifiers"][modifier.name] = dumped_modifier + data['modifiers'] = dump_modifiers(modifiers) gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None) diff --git a/multi_user/libs/replication b/multi_user/libs/replication new file mode 160000 index 0000000..001fbdc --- /dev/null +++ b/multi_user/libs/replication @@ -0,0 +1 @@ +Subproject commit 001fbdc60da58a5e3b7006f1d782d6f472c12809 From e5651151d9a70352be74d1220b8488faf91b314b Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 22 Apr 2021 14:00:26 +0200 Subject: [PATCH 52/66] fix: having both animation and drivers on the same object --- multi_user/bl_types/bl_datablock.py | 17 +++++++++-------- multi_user/libs/replication | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) create mode 160000 multi_user/libs/replication diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index c3ab5a7..348a961 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -161,19 +161,17 @@ class BlDatablock(ReplicatedDatablock): def _dump(self, instance=None): dumper = Dumper() data = {} + animation_data = {} # Dump animation data if has_action(instance): - dumper = Dumper() - dumper.include_filter = ['action'] - data['animation_data'] = dumper.dump(instance.animation_data) - + animation_data['action'] = instance.animation_data.action.name if has_driver(instance): - dumped_drivers = {'animation_data': {'drivers': []}} + animation_data['drivers'] = [] for driver in instance.animation_data.drivers: - dumped_drivers['animation_data']['drivers'].append( - dump_driver(driver)) + animation_data['drivers'].append(dump_driver(driver)) - data.update(dumped_drivers) + if animation_data: + data['animation_data'] = animation_data if self.is_library: data.update(dumper.dump(instance)) @@ -200,6 +198,9 @@ class BlDatablock(ReplicatedDatablock): if 'action' in data['animation_data']: target.animation_data.action = bpy.data.actions[data['animation_data']['action']] + elif target.animation_data.action: + target.animation_data.action = None + # Remove existing animation data if there is not more to load elif hasattr(target, 'animation_data') and target.animation_data: target.animation_data_clear() diff --git a/multi_user/libs/replication b/multi_user/libs/replication new file mode 160000 index 0000000..001fbdc --- /dev/null +++ b/multi_user/libs/replication @@ -0,0 +1 @@ +Subproject commit 001fbdc60da58a5e3b7006f1d782d6f472c12809 From d2108facaba2a82dfa3d35d425b19caf8794ff37 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 22 Apr 2021 14:52:43 +0200 Subject: [PATCH 53/66] feat: fcurve modifiers support --- multi_user/bl_types/bl_action.py | 30 ++++++++++++++++++++++++++++-- tests/test_bl_types/test_action.py | 4 ++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/multi_user/bl_types/bl_action.py b/multi_user/bl_types/bl_action.py index 8672fb6..893fe30 100644 --- a/multi_user/bl_types/bl_action.py +++ b/multi_user/bl_types/bl_action.py @@ -61,7 +61,6 @@ def dump_fcurve(fcurve: bpy.types.FCurve, use_numpy: bool = True) -> dict: points = fcurve.keyframe_points fcurve_data['keyframes_count'] = len(fcurve.keyframe_points) fcurve_data['keyframe_points'] = np_dump_collection(points, KEYFRAME) - else: # Legacy method dumper = Dumper() fcurve_data["keyframe_points"] = [] @@ -71,6 +70,18 @@ def dump_fcurve(fcurve: bpy.types.FCurve, use_numpy: bool = True) -> dict: dumper.dump(k) ) + if fcurve.modifiers: + dumper = Dumper() + dumper.exclude_filter = [ + 'is_valid', + 'active' + ] + dumped_modifiers = [] + for modfifier in fcurve.modifiers: + dumped_modifiers.append(dumper.dump(modfifier)) + + fcurve_data['modifiers'] = dumped_modifiers + return fcurve_data @@ -83,7 +94,7 @@ def load_fcurve(fcurve_data, fcurve): :type fcurve: bpy.types.FCurve """ use_numpy = fcurve_data.get('use_numpy') - + loader = Loader() keyframe_points = fcurve.keyframe_points # Remove all keyframe points @@ -128,6 +139,21 @@ def load_fcurve(fcurve_data, fcurve): fcurve.update() + dumped_fcurve_modifiers = fcurve_data.get('modifiers', None) + + if dumped_fcurve_modifiers: + # clear modifiers + for fmod in fcurve.modifiers: + fcurve.modifiers.remove(fmod) + + # Load each modifiers in order + for modifier_data in dumped_fcurve_modifiers: + modifier = fcurve.modifiers.new(modifier_data['type']) + + loader.load(modifier, modifier_data) + elif fcurve.modifiers: + for fmod in fcurve.modifiers: + fcurve.modifiers.remove(fmod) class BlAction(BlDatablock): bl_id = "actions" diff --git a/tests/test_bl_types/test_action.py b/tests/test_bl_types/test_action.py index 3659777..0c95b8c 100644 --- a/tests/test_bl_types/test_action.py +++ b/tests/test_bl_types/test_action.py @@ -8,6 +8,7 @@ import random from multi_user.bl_types.bl_action import BlAction INTERPOLATION = ['CONSTANT', 'LINEAR', 'BEZIER', 'SINE', 'QUAD', 'CUBIC', 'QUART', 'QUINT', 'EXPO', 'CIRC', 'BACK', 'BOUNCE', 'ELASTIC'] +FMODIFIERS = ['GENERATOR', 'FNGENERATOR', 'ENVELOPE', 'CYCLES', 'NOISE', 'LIMITS', 'STEPPED'] # @pytest.mark.parametrize('blendname', ['test_action.blend']) def test_action(clear_blend): @@ -22,6 +23,9 @@ def test_action(clear_blend): point.co[1] = random.randint(-10,10) point.interpolation = INTERPOLATION[random.randint(0, len(INTERPOLATION)-1)] + for mod_type in FMODIFIERS: + fcurve_sample.modifiers.new(mod_type) + bpy.ops.mesh.primitive_plane_add() bpy.data.objects[0].animation_data_create() bpy.data.objects[0].animation_data.action = datablock From a36c3740cc789ea6f26ee69b36e4d1d9a96d5605 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 22 Apr 2021 15:00:08 +0200 Subject: [PATCH 54/66] fix: load driver variable without id --- multi_user/bl_types/bl_datablock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/multi_user/bl_types/bl_datablock.py b/multi_user/bl_types/bl_datablock.py index 348a961..7cdb2d1 100644 --- a/multi_user/bl_types/bl_datablock.py +++ b/multi_user/bl_types/bl_datablock.py @@ -72,10 +72,10 @@ def load_driver(target_datablock, src_driver): for src_target in src_var_data['targets']: src_target_data = src_var_data['targets'][src_target] - new_var.targets[src_target].id = utils.resolve_from_id( - src_target_data['id'], src_target_data['id_type']) - loader.load( - new_var.targets[src_target], src_target_data) + src_id = src_target_data.get('id') + if src_id: + new_var.targets[src_target].id = utils.resolve_from_id(src_target_data['id'], src_target_data['id_type']) + loader.load(new_var.targets[src_target], src_target_data) # Fcurve new_fcurve = new_driver.keyframe_points From 14779be1ed858f10ebab3e49ab876f5e147d34d1 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 22 Apr 2021 15:52:06 +0200 Subject: [PATCH 55/66] feat: support video file as camera background images --- multi_user/bl_types/bl_camera.py | 20 +++++++++++++++++--- multi_user/bl_types/bl_image.py | 8 ++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/multi_user/bl_types/bl_camera.py b/multi_user/bl_types/bl_camera.py index 486726c..244ad77 100644 --- a/multi_user/bl_types/bl_camera.py +++ b/multi_user/bl_types/bl_camera.py @@ -56,6 +56,11 @@ class BlCamera(BlDatablock): target_img.image = bpy.data.images[img_id] loader.load(target_img, img_data) + img_user = img_data.get('image_user') + if img_user: + loader.load(target_img.image_user, img_user) + + def _dump_implementation(self, data, instance=None): assert(instance) @@ -101,10 +106,19 @@ class BlCamera(BlDatablock): 'scale', 'use_flip_x', 'use_flip_y', - 'image' + 'image_user', + 'image', + 'frame_duration', + 'frame_start', + 'frame_offset', + 'use_cyclic', + 'use_auto_refresh' ] - return dumper.dump(instance) - + data = dumper.dump(instance) + for index, image in enumerate(instance.background_images): + if image.image_user: + data['background_images'][index]['image_user'] = dumper.dump(image.image_user) + return data def _resolve_deps_implementation(self): deps = [] for background in self.instance.background_images: diff --git a/multi_user/bl_types/bl_image.py b/multi_user/bl_types/bl_image.py index c559938..3a248c6 100644 --- a/multi_user/bl_types/bl_image.py +++ b/multi_user/bl_types/bl_image.py @@ -66,9 +66,12 @@ class BlImage(BlDatablock): loader = Loader() loader.load(data, target) - target.source = 'FILE' + target.source = data['source'] target.filepath_raw = get_filepath(data['filename']) - target.colorspace_settings.name = data["colorspace_settings"]["name"] + color_space_name = data["colorspace_settings"]["name"] + + if color_space_name: + target.colorspace_settings.name = color_space_name def _dump(self, instance=None): assert(instance) @@ -83,6 +86,7 @@ class BlImage(BlDatablock): dumper.depth = 2 dumper.include_filter = [ "name", + 'source', 'size', 'height', 'alpha', From 7fe1ae83b1e98cd64503e35eb8613328f726d892 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 23 Apr 2021 11:25:15 +0200 Subject: [PATCH 56/66] feat: update replication version to the right one --- 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 93e2f5c..ef7aaff 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.26'), + ("replication", '0.1.30'), } From dd1c6a4fc7f7a841a98a228cf4fec913d4732667 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 23 Apr 2021 11:45:47 +0200 Subject: [PATCH 57/66] feat: enable back ci --- gitlab-ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 gitlab-ci.yml diff --git a/gitlab-ci.yml b/gitlab-ci.yml new file mode 100644 index 0000000..c7c6485 --- /dev/null +++ b/gitlab-ci.yml @@ -0,0 +1,13 @@ +stages: + - test + - build + - deploy + - doc + + + +include: + - local: .gitlab/ci/test.gitlab-ci.yml + - local: .gitlab/ci/build.gitlab-ci.yml + - local: .gitlab/ci/deploy.gitlab-ci.yml + - local: .gitlab/ci/doc.gitlab-ci.yml From e71af6402cabdd9a158ce93a7daaf8eb4b71ea36 Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 23 Apr 2021 11:46:29 +0200 Subject: [PATCH 58/66] feat: increment addon version --- 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 ef7aaff..8f4011e 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, 3, 0), + "version": (0, 4, 0), "description": "Enable real-time collaborative workflow inside blender", "blender": (2, 82, 0), "location": "3D View > Sidebar > Multi-User tab", From e8cd271bd8a5538e59e70757d8e57d3e48956b7d Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 23 Apr 2021 11:48:01 +0200 Subject: [PATCH 59/66] fix: renable gitlab-ci file --- gitlab-ci.yml => .gitlab-ci.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gitlab-ci.yml => .gitlab-ci.yml (100%) diff --git a/gitlab-ci.yml b/.gitlab-ci.yml similarity index 100% rename from gitlab-ci.yml rename to .gitlab-ci.yml From d9d8ca7ca0c216068e8be0bbe5798660d6561cad Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 23 Apr 2021 15:35:19 +0200 Subject: [PATCH 60/66] revert: image source replication until a proper fix is done --- multi_user/__init__.py | 2 +- multi_user/bl_types/bl_file.py | 2 ++ multi_user/bl_types/bl_image.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 8f4011e..e15a003 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.30'), + ("replication", '0.1.31'), } diff --git a/multi_user/bl_types/bl_file.py b/multi_user/bl_types/bl_file.py index 120048d..26400c3 100644 --- a/multi_user/bl_types/bl_file.py +++ b/multi_user/bl_types/bl_file.py @@ -134,6 +134,8 @@ class BlFile(ReplicatedDatablock): if self.preferences.clear_memory_filecache: return False else: + if not self.instance: + return False memory_size = sys.getsizeof(self.data['file'])-33 disk_size = self.instance.stat().st_size return memory_size != disk_size diff --git a/multi_user/bl_types/bl_image.py b/multi_user/bl_types/bl_image.py index 3a248c6..901a201 100644 --- a/multi_user/bl_types/bl_image.py +++ b/multi_user/bl_types/bl_image.py @@ -66,7 +66,7 @@ class BlImage(BlDatablock): loader = Loader() loader.load(data, target) - target.source = data['source'] + target.source = 'FILE' target.filepath_raw = get_filepath(data['filename']) color_space_name = data["colorspace_settings"]["name"] @@ -86,7 +86,7 @@ class BlImage(BlDatablock): dumper.depth = 2 dumper.include_filter = [ "name", - 'source', + # 'source', 'size', 'height', 'alpha', From 00e7adf02202879ae827273aa452a1ab5e282ee1 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 28 Apr 2021 10:01:04 +0200 Subject: [PATCH 61/66] fix: Image Empty is not loading. Related to #186 --- multi_user/bl_types/bl_object.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 0220646..703e4b9 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -312,13 +312,14 @@ class BlObject(BlDatablock): object_name = data.get("name") data_uuid = data.get("data_uuid") data_id = data.get("data") + data_type = data.get("type") object_data = get_datablock_from_uuid( data_uuid, find_data_from_name(data_id), ignore=['images']) # TODO: use resolve_from_id - if object_data is None and data_uuid: + if data_type != 'EMPTY' and object_data is None: raise Exception(f"Fail to load object {data['name']}({self.uuid})") instance = bpy.data.objects.new(object_name, object_data) From c6eb1ba22f39875d698b329cda4ceafde1370580 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 29 Apr 2021 11:06:46 +0200 Subject: [PATCH 62/66] fix: shapekey performances Related to #187 --- multi_user/bl_types/bl_object.py | 112 +++++++++++++++++++------------ 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 703e4b9..56a4f2f 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -37,6 +37,12 @@ SKIN_DATA = [ 'use_root' ] +SHAPEKEY_BLOCK_ATTR = [ + 'mute', + 'value', + 'slider_min', + 'slider_max', +] if bpy.app.version[1] >= 93: SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float) else: @@ -288,6 +294,66 @@ def load_vertex_groups(dumped_vertex_groups: dict, target_object: bpy.types.Obje for index, weight in vg['vertices']: vertex_group.add([index], weight, 'REPLACE') +def dump_shape_keys(target_key: bpy.types.Key)->dict: + """ Dump the target shape_keys datablock to a dict using numpy + + :param dumped_key: target key datablock + :type dumped_key: bpy.types.Key + :return: dict + """ + + dumped_key_blocks = [] + dumper = Dumper() + dumper.include_filter = [ + 'name', + 'mute', + 'value', + 'slider_min', + 'slider_max', + ] + for key in target_key.key_blocks: + dumped_key_block = dumper.dump(key) + dumped_key_block['data'] = np_dump_collection(key.data, ['co']) + dumped_key_block['relative_key'] = key.relative_key.name + dumped_key_blocks.append(dumped_key_block) + + return { + 'reference_key': target_key.reference_key.name, + 'use_relative': target_key.use_relative, + 'key_blocks': dumped_key_blocks + } + + +def load_shape_keys(dumped_shape_keys: dict, target_object: bpy.types.Object): + """ Load the target shape_keys datablock to a dict using numpy + + :param dumped_key: src key data + :type dumped_key: bpy.types.Key + :param target_object: object used to load the shapekeys data onto + :type target_object: bpy.types.Object + """ + loader = Loader() + # Remove existing ones + target_object.shape_key_clear() + + # Create keys and load vertices coords + dumped_key_blocks = dumped_shape_keys.get('key_blocks') + for dumped_key_block in dumped_key_blocks: + key_block = target_object.shape_key_add(name=dumped_key_block['name']) + + loader.load(key_block, dumped_key_block) + np_load_collection(dumped_key_block['data'], key_block.data, ['co']) + + # Load relative key after all + for dumped_key_block in dumped_key_blocks: + relative_key_name = dumped_key_block.get('relative_key') + key_name = dumped_key_block.get('name') + + target_keyblock = target_object.data.shape_keys.key_blocks[key_name] + relative_key = target_object.data.shape_keys.key_blocks[relative_key_name] + + target_keyblock.relative_key = relative_key + class BlObject(BlDatablock): bl_id = "objects" @@ -345,24 +411,9 @@ class BlObject(BlDatablock): object_data = target.data # SHAPE KEYS - if 'shape_keys' in data: - target.shape_key_clear() - - # Create keys and load vertices coords - for key_block in data['shape_keys']['key_blocks']: - key_data = data['shape_keys']['key_blocks'][key_block] - target.shape_key_add(name=key_block) - - loader.load( - target.data.shape_keys.key_blocks[key_block], key_data) - for vert in key_data['data']: - target.data.shape_keys.key_blocks[key_block].data[vert].co = key_data['data'][vert]['co'] - - # Load relative key after all - for key_block in data['shape_keys']['key_blocks']: - reference = data['shape_keys']['key_blocks'][key_block]['relative_key'] - - target.data.shape_keys.key_blocks[key_block].relative_key = target.data.shape_keys.key_blocks[reference] + shape_keys = data.get('shape_keys') + if shape_keys: + load_shape_keys(shape_keys, target) # Load transformation data loader.load(target, data) @@ -635,30 +686,7 @@ class BlObject(BlDatablock): # SHAPE KEYS object_data = instance.data if hasattr(object_data, 'shape_keys') and object_data.shape_keys: - dumper = Dumper() - dumper.depth = 2 - dumper.include_filter = [ - 'reference_key', - 'use_relative' - ] - data['shape_keys'] = dumper.dump(object_data.shape_keys) - data['shape_keys']['reference_key'] = object_data.shape_keys.reference_key.name - key_blocks = {} - for key in object_data.shape_keys.key_blocks: - dumper.depth = 3 - dumper.include_filter = [ - 'name', - 'data', - 'mute', - 'value', - 'slider_min', - 'slider_max', - 'data', - 'co' - ] - key_blocks[key.name] = dumper.dump(key) - key_blocks[key.name]['relative_key'] = key.relative_key.name - data['shape_keys']['key_blocks'] = key_blocks + data['shape_keys'] = dump_shape_keys(object_data.shape_keys) # SKIN VERTICES if hasattr(object_data, 'skin_vertices') and object_data.skin_vertices: From 9a45fe71250fa823f2f31b217b22ef6baf970487 Mon Sep 17 00:00:00 2001 From: Swann Date: Thu, 29 Apr 2021 14:41:11 +0200 Subject: [PATCH 63/66] fix: shapekey animation data --- multi_user/bl_types/bl_action.py | 45 +++++++++++++++++++++++++++++++- multi_user/bl_types/bl_object.py | 12 ++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/multi_user/bl_types/bl_action.py b/multi_user/bl_types/bl_action.py index 893fe30..7307425 100644 --- a/multi_user/bl_types/bl_action.py +++ b/multi_user/bl_types/bl_action.py @@ -25,7 +25,7 @@ from enum import Enum from .. import utils from .dump_anything import ( Dumper, Loader, np_dump_collection, np_load_collection, remove_items_from_dict) -from .bl_datablock import BlDatablock +from .bl_datablock import BlDatablock, has_action, has_driver, dump_driver, load_driver KEYFRAME = [ @@ -155,6 +155,49 @@ def load_fcurve(fcurve_data, fcurve): for fmod in fcurve.modifiers: fcurve.modifiers.remove(fmod) + +def dump_animation_data(datablock): + animation_data = {} + if has_action(datablock): + animation_data['action'] = datablock.animation_data.action.name + if has_driver(datablock): + animation_data['drivers'] = [] + for driver in datablock.animation_data.drivers: + animation_data['drivers'].append(dump_driver(driver)) + + return animation_data + + +def load_animation_data(animation_data, datablock): + # Load animation data + if animation_data: + if datablock.animation_data is None: + datablock.animation_data_create() + + for d in datablock.animation_data.drivers: + datablock.animation_data.drivers.remove(d) + + if 'drivers' in animation_data: + for driver in animation_data['drivers']: + load_driver(datablock, driver) + + if 'action' in animation_data: + datablock.animation_data.action = bpy.data.actions[animation_data['action']] + elif datablock.animation_data.action: + datablock.animation_data.action = None + + # Remove existing animation data if there is not more to load + elif hasattr(datablock, 'animation_data') and datablock.animation_data: + datablock.animation_data_clear() + + +def resolve_animation_dependencies(datablock): + if has_action(datablock): + return [datablock.animation_data.action] + else: + return [] + + class BlAction(BlDatablock): bl_id = "actions" bl_class = bpy.types.Action diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index c87643b..19d46d2 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 .bl_material import IGNORED_SOCKETS +from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies from .dump_anything import ( Dumper, Loader, @@ -320,7 +321,8 @@ def dump_shape_keys(target_key: bpy.types.Key)->dict: return { 'reference_key': target_key.reference_key.name, 'use_relative': target_key.use_relative, - 'key_blocks': dumped_key_blocks + 'key_blocks': dumped_key_blocks, + 'animation_data': dump_animation_data(target_key) } @@ -354,6 +356,12 @@ def load_shape_keys(dumped_shape_keys: dict, target_object: bpy.types.Object): target_keyblock.relative_key = relative_key + # Shape keys animation data + anim_data = dumped_shape_keys.get('animation_data') + + if anim_data: + load_animation_data(anim_data, target_object.data.shape_keys) + def dump_modifiers(modifiers: bpy.types.bpy_prop_collection)->dict: """ Dump all modifiers of a modifier collection into a dict @@ -760,4 +768,6 @@ class BlObject(BlDatablock): deps.extend(find_textures_dependencies(self.instance.modifiers)) deps.extend(find_geometry_nodes_dependencies(self.instance.modifiers)) + if self.instance.data.shape_keys: + deps.extend(resolve_animation_dependencies(self.instance.data.shape_keys)) return deps From 1e832414947e69d67b491bcbe52b969148784f4b Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 30 Apr 2021 16:26:20 +0200 Subject: [PATCH 64/66] feat: remove pull socket --- 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 e15a003..9fe912e 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.31'), + ("replication", '0.1.33'), } From 771d76a98b513b9e4a39de7fa6df99ae1b7d68cf Mon Sep 17 00:00:00 2001 From: Swann Date: Fri, 30 Apr 2021 16:51:11 +0200 Subject: [PATCH 65/66] fix: missing shapekeys attr --- multi_user/bl_types/bl_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index 19d46d2..84e2b52 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -768,6 +768,6 @@ class BlObject(BlDatablock): deps.extend(find_textures_dependencies(self.instance.modifiers)) deps.extend(find_geometry_nodes_dependencies(self.instance.modifiers)) - if self.instance.data.shape_keys: + if hasattr(self.instance.data, 'shape_keys') and self.instance.data.shape_keys: deps.extend(resolve_animation_dependencies(self.instance.data.shape_keys)) return deps From a34f58ef3f7136dc9e537c6157909c5beb46e688 Mon Sep 17 00:00:00 2001 From: Swann Date: Wed, 2 Jun 2021 23:10:13 +0200 Subject: [PATCH 66/66] fix: cherrypick TCP idle bug --- multi_user/__init__.py | 2 +- multi_user/libs/replication | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 9fe912e..e2cae10 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -44,7 +44,7 @@ from . import environment DEPENDENCIES = { - ("replication", '0.1.33'), + ("replication", '0.1.36'), } diff --git a/multi_user/libs/replication b/multi_user/libs/replication index 001fbdc..8c27d0c 160000 --- a/multi_user/libs/replication +++ b/multi_user/libs/replication @@ -1 +1 @@ -Subproject commit 001fbdc60da58a5e3b7006f1d782d6f472c12809 +Subproject commit 8c27d0cec6b7db1756a7d142c94023fe20f352ff