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