Compare commits

...

3 Commits

Author SHA1 Message Date
Swann Martinez
761fa26ed7 Revert "Merge branch '45-vse-support' into 'master'"
This reverts merge request !68
2020-11-11 19:31:06 +00:00
Swann Martinez
d42ed789e7 Merge branch '45-vse-support' into 'master'
Resolve "VSE Support"

Closes #45

See merge request slumber/multi-user!68
2020-11-11 19:30:37 +00:00
Swann Martinez
bed33ca6ba Merge branch 'develop' into 'master'
v0.1.1

See merge request slumber/multi-user!54
2020-10-16 09:11:20 +00:00
24 changed files with 183 additions and 600 deletions

View File

@ -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 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

@ -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

View File

@ -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'),
}

View File

@ -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 *

View File

@ -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
@ -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()

View File

@ -48,14 +48,11 @@ 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]
target_img.image = bpy.data.images[img_name]
loader.load(target_img, img_data)
def _dump_implementation(self, data, instance=None):

View File

@ -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

View File

@ -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,14 +123,11 @@ 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
def resolve(self):
@ -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):

View File

@ -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)
inputs = node_data.get('inputs')
if inputs:
for idx, inpt in enumerate(inputs):
if hasattr(target_node.inputs[idx], "default_value"):
for input in node_data["inputs"]:
if hasattr(target_node.inputs[input], "default_value"):
try:
target_node.inputs[idx].default_value = inpt["default_value"]
target_node.inputs[input].default_value = node_data["inputs"][input]["default_value"]
except:
logging.error(f"Material input {inpt.keys()} parameter not supported, skipping")
logging.error(
f"Material {input} 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"):
for output in node_data["outputs"]:
if hasattr(target_node.outputs[output], "default_value"):
try:
target_node.outputs[idx].default_value = output["default_value"]
target_node.outputs[output].default_value = node_data["outputs"][output]["default_value"]
except:
logging.error(f"Material output {output.keys()} parameter not supported, skipping")
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):

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
#
# ##### 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)

View File

@ -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)

View File

@ -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
@ -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):

View File

@ -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 <https://www.gnu.org/licenses/>.
#
# ##### 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

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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"}

View File

@ -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

View File

@ -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
@ -301,37 +301,40 @@ class UserSelectionWidget(Widget):
if not ob:
return
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))
position = None
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)

View File

@ -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()

View File

@ -100,8 +100,6 @@ def clean_scene():
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)]