Merge branch 'develop' into feature/event_driven_updates
This commit is contained in:
38
CHANGELOG.md
Normal file
38
CHANGELOG.md
Normal 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.
|
@ -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 ---------------------------------------------------
|
||||
|
@ -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.
|
@ -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"
|
||||
}
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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"]:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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'])
|
||||
|
||||
|
||||
|
@ -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()
|
@ -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),
|
||||
|
Submodule multi_user/libs/replication updated: 343c9d527c...ed2f080bce
@ -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,7 +117,7 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
logger.error(f"Error: {e}")
|
||||
|
||||
finally:
|
||||
settings.is_admin = True
|
||||
|
||||
# Join a session
|
||||
@ -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()
|
||||
|
Reference in New Issue
Block a user