Merge branch 'develop' into feature/event_driven_updates

This commit is contained in:
Swann Martinez
2020-02-28 14:48:09 +01:00
13 changed files with 153 additions and 66 deletions

38
CHANGELOG.md Normal file
View File

@ -0,0 +1,38 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.0.2] - 2020-02-28
### Added
- Blender animation features support (alpha).
- Action.
- Armature (Unstable).
- Shape key.
- Drivers.
- Constraints.
- Snap to user timeline tool.
- Light probes support (only since 2.83).
- Metaballs support.
- Improved modifiers support.
- Online documentation.
- Improved Undo handling.
- Improved overall session handling:
- Time To Leave : ensure clients/server disconnect automatically on connection lost.
- Ping: show clients latency.
- Non-blocking connection.
- Connection state tracking.
- Service communication layer to manage background daemons.
### Changed
- UI revamp:
- Show users frame.
- Expose IPC(inter process communication) port.
- New user list.
- Progress bar to track connection status.
- Right management takes view-layer in account for object selection.
- Use a basic BFS approach for replication graph pre-load.
- Serialization is now based on marshal (2x performance improvements).
- Let pip chose python dependencies install path.

View File

@ -22,7 +22,7 @@ copyright = '2020, Swann Martinez'
author = 'Swann Martinez'
# The full version, including alpha/beta/rc tags
release = '0.0.1'
release = '0.0.2'
# -- General configuration ---------------------------------------------------

View File

@ -4,6 +4,6 @@ Installation
*The process is the same for linux, mac and windows.*
1. Download latest release `multi_user.zip <https://gitlab.com/slumber/multi-user/uploads/8aef79c7cf5b1d9606dc58307fd9ad8b/multi_user.zip>`_.
1. Download latest release `multi_user.zip <https://gitlab.com/slumber/multi-user/uploads/7ce1fd015f50f610e7deefda862d55b1/multi-user.zip>`_.
2. Run blender as administrator (to allow python dependencies auto-installation).
3. Install last_version.zip from your addon preferences.

View File

@ -1,11 +1,15 @@
bl_info = {
"name": "Multi-User",
"author": "Swann Martinez",
"description": "",
"version": (0, 0, 2),
"description": "Enable real-time collaborative workflow inside blender",
"blender": (2, 80, 0),
"location": "",
"location": "3D View > Sidebar > Multi-User tab",
"warning": "Unstable addon, use it at your own risks",
"category": "Collaboration"
"category": "Collaboration",
"wiki_url": "https://multi-user.readthedocs.io/en/develop/index.html",
"tracker_url": "https://gitlab.com/slumber/multi-user/issues",
"support": "COMMUNITY"
}

View File

@ -1,5 +1,6 @@
import bpy
import mathutils
import copy
from .. import utils
from .bl_datablock import BlDatablock
@ -54,18 +55,27 @@ class BlAction(BlDatablock):
# paste dumped keyframes
for dumped_keyframe_point in dumped_fcurve["keyframe_points"]:
if dumped_keyframe_point['type'] == '':
dumped_keyframe_point['type'] = 'KEYFRAME'
new_kf = fcurve.keyframe_points.insert(
dumped_keyframe_point["co"][0] - begin_frame,
dumped_keyframe_point["co"][1],
options={'FAST', 'REPLACE'}
)
keycache = copy.copy(dumped_keyframe_point)
keycache = utils.dump_anything.remove_items_from_dict(
keycache,
["co", "handle_left", "handle_right",'type']
)
loader.load(
new_kf,
utils.dump_anything.remove_items_from_dict(
dumped_keyframe_point,
["co", "handle_left", "handle_right"]
)
keycache
)
new_kf.type = dumped_keyframe_point['type']
new_kf.handle_left = [
dumped_keyframe_point["handle_left"][0] - begin_frame,
dumped_keyframe_point["handle_left"][1]
@ -78,27 +88,29 @@ class BlAction(BlDatablock):
# clearing (needed for blender to update well)
if len(fcurve.keyframe_points) == 0:
target.fcurves.remove(fcurve)
target.id_root= data['id_root']
def dump(self, pointer=None):
assert(pointer)
data = utils.dump_datablock(pointer, 1)
dumper = utils.dump_anything.Dumper()
dumper.depth = 2
dumper.exclude_filter =[
'name_full',
'original',
'use_fake_user',
'user',
'is_library_indirect',
'id_root',
'select_control_point',
'select_right_handle',
'select_left_handle',
'uuid'
'uuid',
'users'
]
dumper.depth = 1
data = dumper.dump(pointer)
data["fcurves"] = []
dumper.depth = 2
for fcurve in self.pointer.fcurves:
fc = {
"data_path": fcurve.data_path,

View File

@ -13,7 +13,7 @@ def load_gpencil_layer(target=None, data=None, create=False):
for frame in data["frames"]:
tframe = target.frames.new(frame)
tframe = target.frames.new(data["frames"][frame]['frame_number'])
# utils.dump_anything.load(tframe, data["frames"][frame])
for stroke in data["frames"][frame]["strokes"]:

View File

@ -78,7 +78,7 @@ class BlMaterial(BlDatablock):
def construct(self, data):
return bpy.data.materials.new(data["name"])
def load(self, data, target):
def load_implementation(self, data, target):
target.name = data['name']
if data['is_grease_pencil']:
if not target.is_grease_pencil:
@ -95,6 +95,8 @@ class BlMaterial(BlDatablock):
target.node_tree.nodes.clear()
utils.dump_anything.load(target,data)
# Load nodes
for node in data["node_tree"]["nodes"]:
load_node(target.node_tree, data["node_tree"]["nodes"][node])
@ -122,6 +124,7 @@ class BlMaterial(BlDatablock):
node_dumper.depth = 1
node_dumper.exclude_filter = [
"dimensions",
"show_expanded"
"select",
"bl_height_min",
"bl_height_max",
@ -140,7 +143,12 @@ class BlMaterial(BlDatablock):
input_dumper.include_filter = ["default_value"]
links_dumper = utils.dump_anything.Dumper()
links_dumper.depth = 3
links_dumper.exclude_filter = ["dimensions"]
links_dumper.include_filter = [
"name",
"to_node",
"from_node",
"from_socket",
"to_socket"]
data = mat_dumper.dump(pointer)
if pointer.use_nodes:

View File

@ -114,6 +114,8 @@ class BlMesh(BlDatablock):
v2 = data["edges"][i]["verts"][1]
edge = mesh_buffer.edges.new([verts[v1], verts[v2]])
edge.smooth = data["edges"][i]["smooth"]
mesh_buffer.edges.ensure_lookup_table()
for p in data["faces"]:
verts = []
for v in data["faces"][p]["verts"]:
@ -151,6 +153,8 @@ class BlMesh(BlDatablock):
dumper.depth = 2
dumper.include_filter = [
'name',
'use_auto_smooth',
'auto_smooth_angle'
]
data = dumper.dump(pointer)
dump_mesh(pointer, data)

View File

@ -7,6 +7,7 @@ from .bl_datablock import BlDatablock
logger = logging.getLogger(__name__)
def load_constraints(target, data):
for local_constraint in target.constraints:
if local_constraint.name not in data:
@ -22,11 +23,13 @@ def load_constraints(target, data):
utils.dump_anything.load(
target_constraint, data[constraint])
def load_pose(target_bone, data):
target_bone.rotation_mode = data['rotation_mode']
utils.dump_anything.load(target_bone, data)
class BlObject(BlDatablock):
bl_id = "objects"
bl_class = bpy.types.Object
@ -75,15 +78,20 @@ class BlObject(BlDatablock):
if bpy.app.version[1] >= 83:
pointer = bpy.data.lightprobes[data["data"]]
else:
logger.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
logger.warning(
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
instance = bpy.data.objects.new(data["name"], pointer)
instance.uuid = self.uuid
return instance
def load_implementation(self, data, target):
if "matrix_world" in data:
target.matrix_world = mathutils.Matrix(data["matrix_world"])
# Load transformation data
rot_mode = 'rotation_quaternion' if data['rotation_mode'] == 'QUATERNION' else 'rotation_euler'
target.rotation_mode = data['rotation_mode']
target.location = data['location']
setattr(target, rot_mode, data[rot_mode])
target.scale = data['scale']
target.name = data["name"]
# Load modifiers
@ -106,7 +114,6 @@ class BlObject(BlDatablock):
if hasattr(target, 'constraints') and 'constraints' in data:
load_constraints(target, data['constraints'])
# Pose
if 'pose' in data:
if not target.pose:
@ -131,12 +138,11 @@ class BlObject(BlDatablock):
load_constraints(
target_bone, bone_data['constraints'])
load_pose(target_bone,bone_data)
load_pose(target_bone, bone_data)
if 'bone_index' in bone_data.keys():
target_bone.bone_group = target.pose.bone_group[bone_data['bone_group_index']]
# Load relations
if 'children' in data.keys():
for child in data['children']:
@ -171,7 +177,8 @@ class BlObject(BlDatablock):
key_data = data['shape_keys']['key_blocks'][key_block]
target.shape_key_add(name=key_block)
utils.dump_anything.load(target.data.shape_keys.key_blocks[key_block],key_data)
utils.dump_anything.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']
@ -195,10 +202,11 @@ class BlObject(BlDatablock):
"empty_display_type",
"empty_display_size",
"instance_collection",
"instance_type"
"instance_type",
"location",
"scale",
'rotation_quaternion' if pointer.rotation_mode == 'QUATERNION' else 'rotation_euler',
]
# if not utils.has_action(pointer):
dumper.include_filter.append('matrix_world')
data = dumper.dump(pointer)
@ -220,8 +228,6 @@ class BlObject(BlDatablock):
dumper.depth = 3
data["constraints"] = dumper.dump(pointer.constraints)
# POSE
if hasattr(pointer, 'pose') and pointer.pose:
# BONES
@ -260,7 +266,6 @@ class BlObject(BlDatablock):
bone_groups[group.name] = dumper.dump(group)
data['pose']['bone_groups'] = bone_groups
# CHILDS
if len(pointer.children) > 0:
childs = []
@ -282,8 +287,6 @@ class BlObject(BlDatablock):
for v in pointer.data.vertices:
for vg in v.groups:
if vg.group == vg_idx:
# logger.error("VG {} : Adding vertex {} to group {}".format(vg_idx, v.index, vg_idx))
vertices.append({
'index': v.index,
'weight': vg.weight
@ -337,7 +340,6 @@ class BlObject(BlDatablock):
if self.is_library:
deps.append(self.pointer.library)
if self.pointer.instance_type == 'COLLECTION':
# TODO: uuid based
deps.append(self.pointer.instance_collection)
@ -346,5 +348,3 @@ class BlObject(BlDatablock):
def is_valid(self):
return bpy.data.objects.get(self.data['name'])

View File

@ -3,7 +3,7 @@ import logging
import bpy
from . import operators, presence, utils
from .libs.replication.replication.constants import FETCHED, RP_COMMON, STATE_ACTIVE, STATE_SYNCING, STATE_SRV_SYNC
from .libs.replication.replication.constants import FETCHED, RP_COMMON, STATE_INITIAL,STATE_QUITTING, STATE_ACTIVE, STATE_SYNCING, STATE_SRV_SYNC
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)
@ -217,6 +217,7 @@ class DrawClient(Draw):
class ClientUpdate(Timer):
def __init__(self, timout=.5):
super().__init__(timout)
self.handle_quit = False
def execute(self):
settings = bpy.context.window_manager.session
@ -276,6 +277,16 @@ class ClientUpdate(Timer):
# TODO: event drivent 3d view refresh
presence.refresh_3d_view()
# ui update
elif session.state['STATE'] == STATE_QUITTING:
presence.refresh_3d_view()
self.handle_quit = True
elif session.state['STATE'] == STATE_INITIAL and self.handle_quit:
self.handle_quit = False
presence.refresh_3d_view()
operators.unregister_delayables()
presence.renderer.stop()
# # ui update
elif session:
presence.refresh_3d_view()

View File

@ -114,6 +114,7 @@ class Dumper:
self._match_type_matrix = (_dump_filter_type(mathutils.Matrix), self._dump_matrix)
self._match_type_vector = (_dump_filter_type(mathutils.Vector), self._dump_vector)
self._match_type_quaternion = (_dump_filter_type(mathutils.Quaternion), self._dump_quaternion)
self._match_type_euler = (_dump_filter_type(mathutils.Euler), self._dump_quaternion)
self._match_type_color = (_dump_filter_type_by_name("Color"), self._dump_color)
self._match_default = (_dump_filter_default, self._dump_default)
@ -193,6 +194,7 @@ class Dumper:
self._match_type_matrix,
self._match_type_vector,
self._match_type_quaternion,
self._match_type_euler,
self._match_type_color,
self._match_default
]
@ -324,6 +326,9 @@ class Loader:
def _load_quaternion(self, quaternion, dump):
quaternion.write(mathutils.Quaternion(dump))
def _load_euler(self, euler, dump):
euler.write(mathutils.Euler(dump))
def _ordered_keys(self, keys):
ordered_keys = []
for order_element in self.order:
@ -354,6 +359,7 @@ class Loader:
(_load_filter_type(mathutils.Matrix, use_bl_rna=False), self._load_matrix), # before float because bl_rna type of matrix if FloatProperty
(_load_filter_type(mathutils.Vector, use_bl_rna=False), self._load_vector), # before float because bl_rna type of vector if FloatProperty
(_load_filter_type(mathutils.Quaternion, use_bl_rna=False), self._load_quaternion),
(_load_filter_type(mathutils.Euler, use_bl_rna=False), self._load_euler),
(_load_filter_type(T.FloatProperty), self._load_identity),
(_load_filter_type(T.StringProperty), self._load_identity),
(_load_filter_type(T.EnumProperty), self._load_identity),

View File

@ -30,6 +30,18 @@ ui_context = None
stop_modal_executor = False
modal_executor_queue = None
server_process = None
def unregister_delayables():
global delayables, stop_modal_executor
for d in delayables:
try:
d.unregister()
except:
continue
stop_modal_executor = True
# OPERATORS
@ -51,7 +63,7 @@ class SessionStartOperator(bpy.types.Operator):
# TODO: Sync server clients
users.clear()
delayables.clear()
# save config
settings.save(context)
@ -105,8 +117,8 @@ class SessionStartOperator(bpy.types.Operator):
except Exception as e:
self.report({'ERROR'}, repr(e))
logger.error(f"Error: {e}")
settings.is_admin = True
finally:
settings.is_admin = True
# Join a session
else:
@ -122,6 +134,8 @@ class SessionStartOperator(bpy.types.Operator):
except Exception as e:
self.report({'ERROR'}, repr(e))
logger.error(f"Error: {e}")
finally:
settings.is_admin = False
# Background client updates service
#TODO: Refactoring
@ -158,18 +172,8 @@ class SessionStopOperator(bpy.types.Operator):
return True
def execute(self, context):
global client, delayables, stop_modal_executor, server_process
global client, delayables, stop_modal_executor
assert(client)
stop_modal_executor = True
settings = context.window_manager.session
settings.is_admin = False
for d in delayables:
try:
d.unregister()
except:
continue
presence.renderer.stop()
try:
client.disconnect()