diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..da2de97 --- /dev/null +++ b/CHANGELOG.md @@ -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. \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 9ae2fa6..b3dd6c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 --------------------------------------------------- diff --git a/docs/getting_started/install.rst b/docs/getting_started/install.rst index cce8a0d..888b2db 100644 --- a/docs/getting_started/install.rst +++ b/docs/getting_started/install.rst @@ -4,6 +4,6 @@ Installation *The process is the same for linux, mac and windows.* -1. Download latest release `multi_user.zip `_. +1. Download latest release `multi_user.zip `_. 2. Run blender as administrator (to allow python dependencies auto-installation). 3. Install last_version.zip from your addon preferences. \ No newline at end of file diff --git a/multi_user/__init__.py b/multi_user/__init__.py index ef130a4..2c8b800 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -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" } diff --git a/multi_user/bl_types/bl_action.py b/multi_user/bl_types/bl_action.py index 8305260..162ccce 100644 --- a/multi_user/bl_types/bl_action.py +++ b/multi_user/bl_types/bl_action.py @@ -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, diff --git a/multi_user/bl_types/bl_gpencil.py b/multi_user/bl_types/bl_gpencil.py index fa1e9ce..1c6c1b7 100644 --- a/multi_user/bl_types/bl_gpencil.py +++ b/multi_user/bl_types/bl_gpencil.py @@ -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"]: diff --git a/multi_user/bl_types/bl_material.py b/multi_user/bl_types/bl_material.py index 50846ec..8d2427e 100644 --- a/multi_user/bl_types/bl_material.py +++ b/multi_user/bl_types/bl_material.py @@ -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: diff --git a/multi_user/bl_types/bl_mesh.py b/multi_user/bl_types/bl_mesh.py index 43667f6..c1527e6 100644 --- a/multi_user/bl_types/bl_mesh.py +++ b/multi_user/bl_types/bl_mesh.py @@ -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) diff --git a/multi_user/bl_types/bl_object.py b/multi_user/bl_types/bl_object.py index af17449..6460483 100644 --- a/multi_user/bl_types/bl_object.py +++ b/multi_user/bl_types/bl_object.py @@ -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,20 +78,25 @@ 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 if hasattr(target, 'modifiers'): - # TODO: smarter selective update + # TODO: smarter selective update target.modifiers.clear() for modifier in data['modifiers']: @@ -106,8 +114,7 @@ class BlObject(BlDatablock): if hasattr(target, 'constraints') and 'constraints' in data: load_constraints(target, data['constraints']) - - # Pose + # Pose if 'pose' in data: if not target.pose: raise Exception('No pose data yet (Fixed in a near futur)') @@ -118,7 +125,7 @@ class BlObject(BlDatablock): if not bg_target: bg_target = target.pose.bone_groups.new(name=bg_name) - + utils.dump_anything.load(bg_target, bg_data) # target.pose.bone_groups.get @@ -130,12 +137,11 @@ class BlObject(BlDatablock): if 'constraints' in bone_data.keys(): 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(): @@ -165,20 +171,21 @@ class BlObject(BlDatablock): target.shape_key_clear() object_data = target.data - + # Create keys and load vertices coords for key_block in data['shape_keys']['key_blocks']: key_data = data['shape_keys']['key_blocks'][key_block] target.shape_key_add(name=key_block) - - 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'] - + # Load relative key after all for key_block in data['shape_keys']['key_blocks']: reference = data['shape_keys']['key_blocks'][key_block]['relative_key'] - + target.data.shape_keys.key_blocks[key_block].relative_key = target.data.shape_keys.key_blocks[reference] def dump_implementation(self, data, pointer=None): @@ -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) @@ -219,8 +227,6 @@ class BlObject(BlDatablock): if hasattr(pointer, 'constraints'): dumper.depth = 3 data["constraints"] = dumper.dump(pointer.constraints) - - # POSE if hasattr(pointer, 'pose') and pointer.pose: @@ -242,7 +248,7 @@ class BlObject(BlDatablock): rotation ] bones[bone.name] = dumper.dump(bone) - + dumper.include_filter = [] dumper.depth = 3 bones[bone.name]["constraints"] = dumper.dump(bone.constraints) @@ -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 = [] @@ -278,23 +283,21 @@ class BlObject(BlDatablock): dumped_vg['name'] = vg.name vertices = [] - + 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 }) - + dumped_vg['vertices'] = vertices vg_data.append(dumped_vg) data['vertex_groups'] = vg_data - + # SHAPE KEYS pointer_data = pointer.data if hasattr(pointer_data, 'shape_keys') and pointer_data.shape_keys: @@ -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']) - - diff --git a/multi_user/delayable.py b/multi_user/delayable.py index de82495..c18c6e4 100644 --- a/multi_user/delayable.py +++ b/multi_user/delayable.py @@ -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,13 +217,14 @@ 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 session_info = bpy.context.window_manager.session session = getattr(operators, 'client', None) renderer = getattr(presence, 'renderer', None) - + if session and renderer and session.state['STATE'] == STATE_ACTIVE: # Check if session has been closes prematurely if session.state['STATE'] == 0: @@ -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() \ No newline at end of file diff --git a/multi_user/libs/dump_anything.py b/multi_user/libs/dump_anything.py index a0502eb..874ebfd 100644 --- a/multi_user/libs/dump_anything.py +++ b/multi_user/libs/dump_anything.py @@ -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 ] @@ -323,6 +325,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 = [] @@ -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), diff --git a/multi_user/libs/replication b/multi_user/libs/replication index 343c9d5..ed2f080 160000 --- a/multi_user/libs/replication +++ b/multi_user/libs/replication @@ -1 +1 @@ -Subproject commit 343c9d527c225e2573ed6add92f6f7d0e8ac9082 +Subproject commit ed2f080bce952db6756016798d606b915bdf67a8 diff --git a/multi_user/operators.py b/multi_user/operators.py index 0000364..0b8bcc0 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -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 - 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() + global client, delayables, stop_modal_executor + assert(client) try: client.disconnect()