Merge branch 'animation-support' into 'develop'

Animation support

See merge request slumber/multi-user!6
This commit is contained in:
Swann Martinez
2020-01-24 13:35:22 +00:00
31 changed files with 1144 additions and 621 deletions

1
log.txt Normal file
View File

@ -0,0 +1 @@
{"area": null, "blend_data":

View File

@ -31,45 +31,45 @@ DEPENDENCIES = {
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR) logger.setLevel(logging.INFO)
#TODO: refactor config #TODO: refactor config
# UTILITY FUNCTIONS # UTILITY FUNCTIONS
def generate_supported_types(): def generate_supported_types():
stype_dict = {'supported_types':{}} stype_dict = {'supported_types':{}}
for type in bl_types.types_to_register(): for type in bl_types.types_to_register():
_type = getattr(bl_types, type) type_module = getattr(bl_types, type)
props = {} type_impl_name = "Bl{}".format(type.split('_')[1].capitalize())
props['bl_delay_refresh']=_type.bl_delay_refresh type_module_class = getattr(type_module, type_impl_name)
props['bl_delay_apply']=_type.bl_delay_apply
props['use_as_filter'] = False
props['icon'] = _type.bl_icon
props['auto_push']=_type.bl_automatic_push
props['bl_name']=_type.bl_id
stype_dict['supported_types'][_type.bl_rep_class.__name__] = props props = {}
props['bl_delay_refresh']=type_module_class.bl_delay_refresh
props['bl_delay_apply']=type_module_class.bl_delay_apply
props['use_as_filter'] = False
props['icon'] = type_module_class.bl_icon
props['auto_push']=type_module_class.bl_automatic_push
props['bl_name']=type_module_class.bl_id
stype_dict['supported_types'][type_impl_name] = props
return stype_dict return stype_dict
def client_list_callback(scene, context): def client_list_callback(scene, context):
from . import operators from . import operators
from .bl_types.bl_user import BlUser
items = [(RP_COMMON, RP_COMMON, "")] items = [(RP_COMMON, RP_COMMON, "")]
username = bpy.context.window_manager.session.username username = bpy.context.window_manager.session.username
cli = operators.client cli = operators.client
if cli: if cli:
client_keys = cli.list(filter=BlUser) client_ids = cli.online_users.keys()
for k in client_keys: for id in client_ids:
name = cli.get(uuid=k).data["name"] name_desc = id
if id == username:
name_desc = name
if name == username:
name_desc += " (self)" name_desc += " (self)"
items.append((name, name_desc, "")) items.append((id, name_desc, ""))
return items return items
@ -90,6 +90,15 @@ class ReplicatedDatablock(bpy.types.PropertyGroup):
auto_push: bpy.props.BoolProperty(default=True) auto_push: bpy.props.BoolProperty(default=True)
icon: bpy.props.StringProperty() icon: bpy.props.StringProperty()
class SessionUser(bpy.types.PropertyGroup):
"""Session User
Blender user information property
"""
username: bpy.props.StringProperty(name="username")
current_frame: bpy.props.IntProperty(name="current_frame")
class SessionProps(bpy.types.PropertyGroup): class SessionProps(bpy.types.PropertyGroup):
username: bpy.props.StringProperty( username: bpy.props.StringProperty(
name="Username", name="Username",
@ -109,25 +118,14 @@ class SessionProps(bpy.types.PropertyGroup):
description='Distant host port', description='Distant host port',
default=5555 default=5555
) )
add_property_depth: bpy.props.IntProperty(
name="add_property_depth",
default=1
)
outliner_filter: bpy.props.StringProperty(name="None")
is_admin: bpy.props.BoolProperty( is_admin: bpy.props.BoolProperty(
name="is_admin", name="is_admin",
default=False default=False
) )
init_scene: bpy.props.BoolProperty(
name="init_scene",
default=True
)
start_empty: bpy.props.BoolProperty( start_empty: bpy.props.BoolProperty(
name="start_empty", name="start_empty",
default=True default=True
) )
active_object: bpy.props.PointerProperty(
name="active_object", type=bpy.types.Object)
session_mode: bpy.props.EnumProperty( session_mode: bpy.props.EnumProperty(
name='session_mode', name='session_mode',
description='session mode', description='session mode',
@ -179,10 +177,11 @@ class SessionProps(bpy.types.PropertyGroup):
description='Show only owned datablocks', description='Show only owned datablocks',
default=True default=True
) )
use_select_right: bpy.props.BoolProperty( user_snap_running: bpy.props.BoolProperty(
name="Selection right", default=False
description='Change right on selection', )
default=True time_snap_running: bpy.props.BoolProperty(
default=False
) )
def load(self): def load(self):
@ -239,6 +238,7 @@ class SessionProps(bpy.types.PropertyGroup):
classes = ( classes = (
SessionUser,
ReplicatedDatablock, ReplicatedDatablock,
SessionProps, SessionProps,
@ -267,7 +267,10 @@ def register():
bpy.types.WindowManager.session = bpy.props.PointerProperty( bpy.types.WindowManager.session = bpy.props.PointerProperty(
type=SessionProps) type=SessionProps)
bpy.types.ID.uuid = bpy.props.StringProperty(default="") bpy.types.ID.uuid = bpy.props.StringProperty(default="")
bpy.types.WindowManager.online_users = bpy.props.CollectionProperty(
type=SessionUser
)
bpy.types.WindowManager.user_index = bpy.props.IntProperty()
bpy.context.window_manager.session.load() bpy.context.window_manager.session.load()
presence.register() presence.register()

View File

@ -1,5 +1,4 @@
__all__ = [ __all__ = [
'bl_user',
'bl_object', 'bl_object',
'bl_mesh', 'bl_mesh',
'bl_camera', 'bl_camera',

View File

@ -7,22 +7,77 @@ from .bl_datablock import BlDatablock
# WIP # WIP
class BlAction(BlDatablock): class BlAction(BlDatablock):
def load(self, data, target): bl_id = "actions"
utils.dump_anything.load(target, data) bl_class = bpy.types.Action
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'ACTION_TWEAK'
def construct(self, data): def construct(self, data):
return bpy.data.actions.new(data["name"]) return bpy.data.actions.new(data["name"])
def load(self, data, target): def load(self, data, target):
pass begin_frame = 100000
# # find target object end_frame = -100000
# object_ = bpy.context.scene.objects.active
# if object_ is None: for dumped_fcurve in data["fcurves"]:
# raise RuntimeError("Nothing is selected.") begin_frame = min(
# if object_.mode != 'POSE': # object must be in pose mode begin_frame,
# raise RuntimeError("Object must be in pose mode.") min(
# if object_.animation_data.action is None: [begin_frame] + [dkp["co"][0] for dkp in dumped_fcurve["keyframe_points"]]
# raise RuntimeError("Object needs an active action.") )
)
end_frame = max(
end_frame,
max(
[end_frame] + [dkp["co"][0] for dkp in dumped_fcurve["keyframe_points"]]
)
)
begin_frame = 0
loader = utils.dump_anything.Loader()
for dumped_fcurve in data["fcurves"]:
dumped_data_path = dumped_fcurve["data_path"]
dumped_array_index = dumped_fcurve["dumped_array_index"]
# create fcurve if needed
fcurve = target.fcurves.find(dumped_data_path, index=dumped_array_index)
if fcurve is None:
fcurve = target.fcurves.new(dumped_data_path, index=dumped_array_index)
# remove keyframes within dumped_action range
for keyframe in reversed(fcurve.keyframe_points):
if end_frame >= (keyframe.co[0] + begin_frame ) >= begin_frame:
fcurve.keyframe_points.remove(keyframe, fast=True)
# paste dumped keyframes
for dumped_keyframe_point in dumped_fcurve["keyframe_points"]:
new_kf = fcurve.keyframe_points.insert(
dumped_keyframe_point["co"][0] - begin_frame,
dumped_keyframe_point["co"][1],
options={'FAST', 'REPLACE'}
)
loader.load(
new_kf,
utils.dump_anything.remove_items_from_dict(
dumped_keyframe_point,
["co", "handle_left", "handle_right"]
)
)
new_kf.handle_left = [
dumped_keyframe_point["handle_left"][0] - begin_frame,
dumped_keyframe_point["handle_left"][1]
]
new_kf.handle_right = [
dumped_keyframe_point["handle_right"][0] - begin_frame,
dumped_keyframe_point["handle_right"][1]
]
# clearing (needed for blender to update well)
if len(fcurve.keyframe_points) == 0:
target.fcurves.remove(fcurve)
def dump(self, pointer=None): def dump(self, pointer=None):
assert(pointer) assert(pointer)
@ -30,7 +85,18 @@ class BlAction(BlDatablock):
dumper = utils.dump_anything.Dumper() dumper = utils.dump_anything.Dumper()
dumper.depth = 2 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'
]
data["fcurves"] = [] data["fcurves"] = []
for fcurve in self.pointer.fcurves: for fcurve in self.pointer.fcurves:
@ -49,20 +115,7 @@ class BlAction(BlDatablock):
return data return data
def resolve(self):
assert(self.data)
self.pointer = bpy.data.actions.get(self.data['name'])
def diff(self):
return False
def is_valid(self): def is_valid(self):
return bpy.data.actions.get(self.data['name']) return bpy.data.actions.get(self.data['name'])
bl_id = "actions"
bl_class = bpy.types.Action
bl_rep_class = BlAction
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'ACTION_TWEAK'

View File

@ -3,104 +3,129 @@ import mathutils
from ..libs.overrider import Overrider from ..libs.overrider import Overrider
from .. import utils from .. import utils
from .. import presence from .. import presence, operators
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
# WIP # WIP
class BlArmature(BlDatablock): class BlArmature(BlDatablock):
bl_id = "armatures"
bl_class = bpy.types.Armature
bl_delay_refresh = 1
bl_delay_apply = 0
bl_automatic_push = True
bl_icon = 'ARMATURE_DATA'
def construct(self, data): def construct(self, data):
return bpy.data.armatures.new(data["name"]) return bpy.data.armatures.new(data["name"])
def load(self, data, target): def load_implementation(self, data, target):
# Load parent object # Load parent object
if data['user'] not in bpy.data.objects.keys(): parent_object = utils.find_from_attr(
parent_object = bpy.data.objects.new(data['user'], self.pointer) 'uuid',
else: data['user'],
parent_object = bpy.data.objects[data['user']] bpy.data.objects
)
is_object_in_master = (data['user_collection'][0] == "Master Collection") if parent_object is None:
#TODO: recursive parent collection loading parent_object = bpy.data.objects.new(
data['user_name'], self.pointer)
parent_object.uuid = data['user']
is_object_in_master = (
data['user_collection'][0] == "Master Collection")
# TODO: recursive parent collection loading
# Link parent object to the collection # Link parent object to the collection
if is_object_in_master: if is_object_in_master:
parent_collection = bpy.data.scenes[data['user_scene'][0]].collection parent_collection = bpy.data.scenes[data['user_scene']
elif data['user_collection'][0] not in bpy.data.collections.keys(): [0]].collection
parent_collection = bpy.data.collections.new(data['user_collection'][0]) elif data['user_collection'][0] not in bpy.data.collections.keys():
parent_collection = bpy.data.collections.new(
data['user_collection'][0])
else: else:
parent_collection = bpy.data.collections[data['user_collection'][0]] parent_collection = bpy.data.collections[data['user_collection'][0]]
if parent_object.name not in parent_collection.objects: if parent_object.name not in parent_collection.objects:
parent_collection.objects.link(parent_object) parent_collection.objects.link(parent_object)
# Link parent collection to the scene master collection # Link parent collection to the scene master collection
if not is_object_in_master and parent_collection.name not in bpy.data.scenes[data['user_scene'][0]].collection.children: if not is_object_in_master and parent_collection.name not in bpy.data.scenes[data['user_scene'][0]].collection.children:
bpy.data.scenes[data['user_scene'][0]].collection. children.link(parent_collection) bpy.data.scenes[data['user_scene'][0]
].collection. children.link(parent_collection)
# utils.dump_anything.load(target, data)
# with Overrider(name="bpy_",parent=bpy.context) as bpy_:
area, region, rv3d = presence.view3d_find()
current_mode = bpy.context.mode
current_active_object = bpy.context.view_layer.objects.active
# LOAD ARMATURE BONES
if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.view_layer.objects.active = parent_object bpy.context.view_layer.objects.active = parent_object
# override = bpy.context.copy()
# override['window'] = bpy.data.window_managers[0].windows[0]
# override['mode'] = 'EDIT_ARMATURE'
# override['window_manager'] = bpy.data.window_managers[0]
# override['area'] = area
# override['region'] = region
# override['screen'] = bpy.data.window_managers[0].windows[0].screen
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
for bone in data['bones']: for bone in data['bones']:
if bone not in self.pointer.edit_bones: if bone not in self.pointer.edit_bones:
new_bone = self.pointer.edit_bones.new(bone) new_bone = self.pointer.edit_bones.new(bone)
else: else:
new_bone = self.pointer.edit_bones[bone] new_bone = self.pointer.edit_bones[bone]
new_bone.tail = data['bones'][bone]['tail_local'] bone_data = data['bones'].get(bone)
new_bone.head = data['bones'][bone]['head_local']
new_bone.tail_radius = data['bones'][bone]['tail_radius']
new_bone.head_radius = data['bones'][bone]['head_radius']
if 'parent' in data['bones'][bone]: new_bone.tail = bone_data['tail_local']
new_bone.parent = self.pointer.edit_bones[data['bones'][bone]['parent']['name']] new_bone.head = bone_data['head_local']
new_bone.use_connect = data['bones'][bone]['use_connect'] new_bone.tail_radius = bone_data['tail_radius']
bpy.ops.object.mode_set(mode='OBJECT') new_bone.head_radius = bone_data['head_radius']
# bpy_.mode = 'EDIT_ARMATURE' if 'parent' in bone_data:
new_bone.parent = self.pointer.edit_bones[data['bones']
[bone]['parent']]
new_bone.use_connect = bone_data['use_connect']
# bpy_.active_object = armature utils.dump_anything.load(new_bone, bone_data)
# bpy_.selected_objects = [armature]
def dump(self, pointer=None): if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.view_layer.objects.active = current_active_object
# TODO: clean way to restore previous context
if 'EDIT' in current_mode:
bpy.ops.object.mode_set(mode='EDIT')
def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
data = utils.dump_datablock(pointer, 4)
#get the parent Object dumper = utils.dump_anything.Dumper()
dumper.depth = 4
dumper.include_filter = [
'bones',
'tail_local',
'head_local',
'tail_radius',
'head_radius',
'use_connect',
'parent',
'name',
'layers'
]
data = dumper.dump(pointer)
for bone in pointer.bones:
if bone.parent:
data['bones'][bone.name]['parent'] = bone.parent.name
# get the parent Object
object_users = utils.get_datablock_users(pointer)[0] object_users = utils.get_datablock_users(pointer)[0]
data['user'] = object_users.name data['user'] = object_users.uuid
data['user_name'] = object_users.name
#get parent collection # get parent collection
container_users = utils.get_datablock_users(object_users) container_users = utils.get_datablock_users(object_users)
data['user_collection'] = [item.name for item in container_users if isinstance(item,bpy.types.Collection)] data['user_collection'] = [
data['user_scene'] = [item.name for item in container_users if isinstance(item,bpy.types.Scene)] item.name for item in container_users if isinstance(item, bpy.types.Collection)]
data['user_scene'] = [
item.name for item in container_users if isinstance(item, bpy.types.Scene)]
return data return data
def resolve(self):
assert(self.data)
self.pointer = bpy.data.armatures.get(self.data['name'])
def diff(self):
False
def is_valid(self): def is_valid(self):
return bpy.data.armatures.get(self.data['name']) return bpy.data.armatures.get(self.data['name'])
bl_id = "armatures"
bl_class = bpy.types.Armature
bl_rep_class = BlArmature
bl_delay_refresh = 1
bl_delay_apply = 0
bl_automatic_push = True
bl_icon = 'ARMATURE_DATA'

View File

@ -6,6 +6,13 @@ from .bl_datablock import BlDatablock
class BlCamera(BlDatablock): class BlCamera(BlDatablock):
bl_id = "cameras"
bl_class = bpy.types.Camera
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'CAMERA_DATA'
def load(self, data, target): def load(self, data, target):
utils.dump_anything.load(target, data) utils.dump_anything.load(target, data)
@ -18,7 +25,7 @@ class BlCamera(BlDatablock):
def construct(self, data): def construct(self, data):
return bpy.data.cameras.new(data["name"]) return bpy.data.cameras.new(data["name"])
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
dumper = utils.dump_anything.Dumper() dumper = utils.dump_anything.Dumper()
@ -45,17 +52,5 @@ class BlCamera(BlDatablock):
] ]
return dumper.dump(pointer) return dumper.dump(pointer)
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.cameras)
def is_valid(self): def is_valid(self):
return bpy.data.cameras.get(self.data['name']) return bpy.data.cameras.get(self.data['name'])
bl_id = "cameras"
bl_class = bpy.types.Camera
bl_rep_class = BlCamera
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'CAMERA_DATA'

View File

@ -6,6 +6,13 @@ from .bl_datablock import BlDatablock
class BlCollection(BlDatablock): class BlCollection(BlDatablock):
bl_id = "collections"
bl_icon = 'FILE_FOLDER'
bl_class = bpy.types.Collection
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
def construct(self, data): def construct(self, data):
if self.is_library: if self.is_library:
with bpy.data.libraries.load(filepath=bpy.data.libraries[self.data['library']].filepath, link=True) as (sourceData, targetData): with bpy.data.libraries.load(filepath=bpy.data.libraries[self.data['library']].filepath, link=True) as (sourceData, targetData):
@ -47,7 +54,7 @@ class BlCollection(BlDatablock):
if collection.uuid not in data["children"]: if collection.uuid not in data["children"]:
target.children.unlink(collection) target.children.unlink(collection)
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
data = {} data = {}
data['name'] = pointer.name data['name'] = pointer.name
@ -68,19 +75,8 @@ class BlCollection(BlDatablock):
data['children'] = collection_children data['children'] = collection_children
# dumper = utils.dump_anything.Dumper()
# dumper.depth = 2
# dumper.include_filter = ['name','objects', 'children']
# return dumper.dump(pointer)
return data return data
def resolve(self):
self.pointer = utils.find_from_attr(
'uuid',
self.uuid,
bpy.data.collections)
def resolve_dependencies(self): def resolve_dependencies(self):
deps = [] deps = []
@ -94,11 +90,3 @@ class BlCollection(BlDatablock):
def is_valid(self): def is_valid(self):
return bpy.data.collections.get(self.data['name']) return bpy.data.collections.get(self.data['name'])
bl_id = "collections"
bl_icon = 'FILE_FOLDER'
bl_class = bpy.types.Collection
bl_rep_class = BlCollection
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True

View File

@ -5,6 +5,13 @@ from .. import utils
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
class BlCurve(BlDatablock): class BlCurve(BlDatablock):
bl_id = "curves"
bl_class = bpy.types.Curve
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'CURVE_DATA'
def construct(self, data): def construct(self, data):
return bpy.data.curves.new(data["name"], 'CURVE') return bpy.data.curves.new(data["name"], 'CURVE')
@ -29,7 +36,7 @@ class BlCurve(BlDatablock):
utils.dump_anything.load( utils.dump_anything.load(
new_spline.points[point_index], data['splines'][spline]["points"][point_index]) new_spline.points[point_index], data['splines'][spline]["points"][point_index])
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
data = utils.dump_datablock(pointer, 1) data = utils.dump_datablock(pointer, 1)
data['splines'] = {} data['splines'] = {}
@ -52,15 +59,5 @@ class BlCurve(BlDatablock):
data['type'] = 'CURVE' data['type'] = 'CURVE'
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.curves)
def is_valid(self): def is_valid(self):
return bpy.data.curves.get(self.data['name']) return bpy.data.curves.get(self.data['name'])
bl_id = "curves"
bl_class = bpy.types.Curve
bl_rep_class = BlCurve
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'CURVE_DATA'

View File

@ -6,7 +6,63 @@ from ..libs.replication.replication.data import ReplicatedDatablock
from ..libs.replication.replication.constants import UP from ..libs.replication.replication.constants import UP
def dump_driver(driver):
dumper = utils.dump_anything.Dumper()
dumper.depth = 6
data = dumper.dump(driver)
return data
def load_driver(target_datablock, src_driver):
drivers = target_datablock.animation_data.drivers
src_driver_data = src_driver['driver']
new_driver = drivers.new(src_driver['data_path'])
# Settings
new_driver.driver.type = src_driver_data['type']
new_driver.driver.expression = src_driver_data['expression']
utils.dump_anything.load(new_driver, src_driver)
# Variables
for src_variable in src_driver_data['variables']:
src_var_data = src_driver_data['variables'][src_variable]
new_var = new_driver.driver.variables.new()
new_var.name = src_var_data['name']
new_var.type = src_var_data['type']
for src_target in src_var_data['targets']:
src_target_data = src_var_data['targets'][src_target]
new_var.targets[src_target].id = utils.resolve_from_id(
src_target_data['id'], src_target_data['id_type'])
utils.dump_anything.load(
new_var.targets[src_target], src_target_data)
# Fcurve
new_fcurve = new_driver.keyframe_points
for p in reversed(new_fcurve):
new_fcurve.remove(p, fast=True)
new_fcurve.add(len(src_driver['keyframe_points']))
for index, src_point in enumerate(src_driver['keyframe_points']):
new_point = new_fcurve[index]
utils.dump_anything.load(
new_point, src_driver['keyframe_points'][src_point])
class BlDatablock(ReplicatedDatablock): class BlDatablock(ReplicatedDatablock):
"""BlDatablock
bl_id : blender internal storage identifier
bl_class : blender internal type
bl_delay_refresh : refresh rate in second for observers
bl_delay_apply : refresh rate in sec for apply
bl_automatic_push : boolean
bl_icon : type icon (blender icon name)
"""
bl_id = "scenes"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
pointer = kwargs.get('pointer', None) pointer = kwargs.get('pointer', None)
@ -14,7 +70,7 @@ class BlDatablock(ReplicatedDatablock):
# TODO: use is_library_indirect # TODO: use is_library_indirect
self.is_library = (pointer and hasattr(pointer, 'library') and self.is_library = (pointer and hasattr(pointer, 'library') and
pointer.library) or \ pointer.library) or \
(self.data and 'library' in self.data) (self.data and 'library' in self.data)
if self.is_library: if self.is_library:
self.load = self.load_library self.load = self.load_library
@ -50,10 +106,68 @@ class BlDatablock(ReplicatedDatablock):
def resolve_dependencies_library(self): def resolve_dependencies_library(self):
return [self.pointer.library] return [self.pointer.library]
def resolve(self):
datablock_ref = None
datablock_root = getattr(bpy.data, self.bl_id)
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
# In case of lost uuid (ex: undo), resolve by name and reassign it
# TODO: avoid reference storing
if not datablock_ref:
datablock_ref = getattr(
bpy.data, self.bl_id).get(self.data['name'])
if datablock_ref:
setattr(datablock_ref, 'uuid', self.uuid)
self.pointer = datablock_ref
def dump(self, pointer=None):
data = {}
if utils.has_action(pointer):
dumper = utils.dump_anything.Dumper()
dumper.include_filter = ['action']
data['animation_data'] = dumper.dump(pointer.animation_data)
if utils.has_driver(pointer):
dumped_drivers = {'animation_data': {'drivers': []}}
for driver in pointer.animation_data.drivers:
dumped_drivers['animation_data']['drivers'].append(
dump_driver(driver))
data.update(dumped_drivers)
data.update(self.dump_implementation(data, pointer=pointer))
return data
def dump_implementation(self, data, target):
raise NotImplementedError
def load(self, data, target):
# Load animation data
if 'animation_data' in data.keys():
if target.animation_data is None:
target.animation_data_create()
for d in target.animation_data.drivers:
target.animation_data.drivers.remove(d)
if 'drivers' in data['animation_data']:
for driver in data['animation_data']['drivers']:
load_driver(target, driver)
if 'action' in data['animation_data']:
target.animation_data.action = bpy.data.actions[data['animation_data']['action']]
self.load_implementation(data, target)
def load_implementation(self, data, target):
raise NotImplementedError
def resolve_dependencies(self): def resolve_dependencies(self):
dependencies = [] dependencies = []
if hasattr(self.pointer,'animation_data') and self.pointer.animation_data: if utils.has_action(self.pointer):
dependencies.append(self.pointer.animation_data.action) dependencies.append(self.pointer.animation_data.action)
return dependencies return dependencies

View File

@ -34,6 +34,13 @@ def load_gpencil_layer(target=None, data=None, create=False):
class BlGpencil(BlDatablock): class BlGpencil(BlDatablock):
bl_id = "grease_pencils"
bl_class = bpy.types.GreasePencil
bl_delay_refresh = 5
bl_delay_apply = 5
bl_automatic_push = True
bl_icon = 'GREASEPENCIL'
def construct(self, data): def construct(self, data):
return bpy.data.grease_pencils.new(data["name"]) return bpy.data.grease_pencils.new(data["name"])
@ -57,16 +64,13 @@ class BlGpencil(BlDatablock):
for mat in data['materials']: for mat in data['materials']:
target.materials.append(bpy.data.materials[mat]) target.materials.append(bpy.data.materials[mat])
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
data = utils.dump_datablock(pointer, 2) data = utils.dump_datablock(pointer, 2)
utils.dump_datablock_attibutes( utils.dump_datablock_attibutes(
pointer, ['layers'], 9, data) pointer, ['layers'], 9, data)
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.grease_pencils)
def resolve_dependencies(self): def resolve_dependencies(self):
deps = [] deps = []
@ -77,11 +81,3 @@ class BlGpencil(BlDatablock):
def is_valid(self): def is_valid(self):
return bpy.data.grease_pencils.get(self.data['name']) return bpy.data.grease_pencils.get(self.data['name'])
bl_id = "grease_pencils"
bl_class = bpy.types.GreasePencil
bl_rep_class = BlGpencil
bl_delay_refresh = 5
bl_delay_apply = 5
bl_automatic_push = True
bl_icon = 'GREASEPENCIL'

View File

@ -24,6 +24,13 @@ def dump_image(image):
return pixels return pixels
class BlImage(BlDatablock): class BlImage(BlDatablock):
bl_id = "images"
bl_class = bpy.types.Image
bl_delay_refresh = 0
bl_delay_apply = 0
bl_automatic_push = False
bl_icon = 'IMAGE_DATA'
def construct(self, data): def construct(self, data):
return bpy.data.images.new( return bpy.data.images.new(
name=data['name'], name=data['name'],
@ -46,7 +53,7 @@ class BlImage(BlDatablock):
image.filepath = img_path image.filepath = img_path
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
data = {} data = {}
data['pixels'] = dump_image(pointer) data['pixels'] = dump_image(pointer)
@ -58,18 +65,8 @@ class BlImage(BlDatablock):
data) data)
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.images)
def diff(self): def diff(self):
return False return False
def is_valid(self): def is_valid(self):
return bpy.data.images.get(self.data['name']) return bpy.data.images.get(self.data['name'])
bl_id = "images"
bl_class = bpy.types.Image
bl_rep_class = BlImage
bl_delay_refresh = 0
bl_delay_apply = 0
bl_automatic_push = False
bl_icon = 'IMAGE_DATA'

View File

@ -6,6 +6,13 @@ from .bl_datablock import BlDatablock
class BlLattice(BlDatablock): class BlLattice(BlDatablock):
bl_id = "lattices"
bl_class = bpy.types.Lattice
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LATTICE_DATA'
def load(self, data, target): def load(self, data, target):
utils.dump_anything.load(target, data) utils.dump_anything.load(target, data)
@ -38,17 +45,8 @@ class BlLattice(BlDatablock):
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lattices)
def is_valid(self): def is_valid(self):
return bpy.data.lattices.get(self.data['name']) return bpy.data.lattices.get(self.data['name'])
bl_id = "lattices"
bl_class = bpy.types.Lattice
bl_rep_class = BlLattice
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LATTICE_DATA'

View File

@ -6,6 +6,13 @@ from .bl_datablock import BlDatablock
class BlLibrary(BlDatablock): class BlLibrary(BlDatablock):
bl_id = "libraries"
bl_class = bpy.types.Library
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LIBRARY_DATA_DIRECT'
def construct(self, data): def construct(self, data):
with bpy.data.libraries.load(filepath=data["filepath"], link=True) as (sourceData, targetData): with bpy.data.libraries.load(filepath=data["filepath"], link=True) as (sourceData, targetData):
targetData = sourceData targetData = sourceData
@ -17,16 +24,5 @@ class BlLibrary(BlDatablock):
assert(pointer) assert(pointer)
return utils.dump_datablock(pointer, 1) return utils.dump_datablock(pointer, 1)
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.libraries)
def is_valid(self): def is_valid(self):
return bpy.data.libraries.get(self.data['name']) return bpy.data.libraries.get(self.data['name'])
bl_id = "libraries"
bl_class = bpy.types.Library
bl_rep_class = BlLibrary
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LIBRARY_DATA_DIRECT'

View File

@ -6,13 +6,20 @@ from .bl_datablock import BlDatablock
class BlLight(BlDatablock): class BlLight(BlDatablock):
bl_id = "lights"
bl_class = bpy.types.Light
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LIGHT_DATA'
def construct(self, data): def construct(self, data):
return bpy.data.lights.new(data["name"], data["type"]) return bpy.data.lights.new(data["name"], data["type"])
def load(self, data, target): def load(self, data, target):
utils.dump_anything.load(target, data) utils.dump_anything.load(target, data)
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
dumper = utils.dump_anything.Dumper() dumper = utils.dump_anything.Dumper()
dumper.depth = 3 dumper.depth = 3
@ -39,16 +46,6 @@ class BlLight(BlDatablock):
data = dumper.dump(pointer) data = dumper.dump(pointer)
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lights)
def is_valid(self): def is_valid(self):
return bpy.data.lights.get(self.data['name']) return bpy.data.lights.get(self.data['name'])
bl_id = "lights"
bl_class = bpy.types.Light
bl_rep_class = BlLight
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LIGHT_DATA'

View File

@ -5,12 +5,20 @@ from .. import utils
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
class BlLightProbe(BlDatablock): class BlLightprobe(BlDatablock):
bl_id = "lightprobes"
bl_class = bpy.types.LightProbe
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LIGHTPROBE_GRID'
def load(self, data, target): def load(self, data, target):
utils.dump_anything.load(target, data) utils.dump_anything.load(target, data)
def construct(self, data): def construct(self, data):
return bpy.data.lightprobes.new(data["name"]) type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type']
return bpy.data.lightprobes.new(data["name"], type)
def dump(self, pointer=None): def dump(self, pointer=None):
assert(pointer) assert(pointer)
@ -19,6 +27,7 @@ class BlLightProbe(BlDatablock):
dumper.depth = 1 dumper.depth = 1
dumper.include_filter = [ dumper.include_filter = [
"name", "name",
'type',
'influence_type', 'influence_type',
'influence_distance', 'influence_distance',
'falloff', 'falloff',
@ -29,21 +38,15 @@ class BlLightProbe(BlDatablock):
'use_custom_parallax', 'use_custom_parallax',
'parallax_type', 'parallax_type',
'parallax_distance', 'parallax_distance',
'grid_resolution_x',
'grid_resolution_y',
'grid_resolution_z',
'visibility_buffer_bias',
'visibility_bleed_bias',
'visibility_blur'
] ]
return dumper.dump(pointer) return dumper.dump(pointer)
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lattices)
def is_valid(self): def is_valid(self):
return bpy.data.lattices.get(self.data['name']) return bpy.data.lattices.get(self.data['name'])
bl_id = "lightprobes"
bl_class = bpy.types.LightProbe
bl_rep_class = BlLightProbe
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LIGHTPROBE_GRID'

View File

@ -68,6 +68,13 @@ def load_link(target_node_tree, source):
class BlMaterial(BlDatablock): class BlMaterial(BlDatablock):
bl_id = "materials"
bl_class = bpy.types.Material
bl_delay_refresh = 10
bl_delay_apply = 10
bl_automatic_push = True
bl_icon = 'MATERIAL_DATA'
def construct(self, data): def construct(self, data):
return bpy.data.materials.new(data["name"]) return bpy.data.materials.new(data["name"])
@ -98,7 +105,7 @@ class BlMaterial(BlDatablock):
for link in data["node_tree"]["links"]: for link in data["node_tree"]["links"]:
load_link(target.node_tree, data["node_tree"]["links"][link]) load_link(target.node_tree, data["node_tree"]["links"][link])
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
mat_dumper = utils.dump_anything.Dumper() mat_dumper = utils.dump_anything.Dumper()
mat_dumper.depth = 2 mat_dumper.depth = 2
@ -175,9 +182,6 @@ class BlMaterial(BlDatablock):
utils.dump_datablock_attibutes(pointer, ["grease_pencil"], 3, data) utils.dump_datablock_attibutes(pointer, ["grease_pencil"], 3, data)
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.materials)
def resolve_dependencies(self): def resolve_dependencies(self):
# TODO: resolve node group deps # TODO: resolve node group deps
deps = [] deps = []
@ -194,11 +198,3 @@ class BlMaterial(BlDatablock):
def is_valid(self): def is_valid(self):
return bpy.data.materials.get(self.data['name']) return bpy.data.materials.get(self.data['name'])
bl_id = "materials"
bl_class = bpy.types.Material
bl_rep_class = BlMaterial
bl_delay_refresh = 10
bl_delay_apply = 10
bl_automatic_push = True
bl_icon = 'MATERIAL_DATA'

View File

@ -75,6 +75,13 @@ def dump_mesh(mesh, data={}):
return mesh_data return mesh_data
class BlMesh(BlDatablock): class BlMesh(BlDatablock):
bl_id = "meshes"
bl_class = bpy.types.Mesh
bl_delay_refresh = 10
bl_delay_apply = 10
bl_automatic_push = True
bl_icon = 'MESH_DATA'
def construct(self, data): def construct(self, data):
instance = bpy.data.meshes.new(data["name"]) instance = bpy.data.meshes.new(data["name"])
instance.uuid = self.uuid instance.uuid = self.uuid
@ -139,7 +146,7 @@ class BlMesh(BlDatablock):
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
data = utils.dump_datablock(pointer, 2) data = utils.dump_datablock(pointer, 2)
@ -154,9 +161,6 @@ class BlMesh(BlDatablock):
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.meshes)
def resolve_dependencies(self): def resolve_dependencies(self):
deps = [] deps = []
@ -169,10 +173,3 @@ class BlMesh(BlDatablock):
def is_valid(self): def is_valid(self):
return bpy.data.meshes.get(self.data['name']) return bpy.data.meshes.get(self.data['name'])
bl_id = "meshes"
bl_class = bpy.types.Mesh
bl_rep_class = BlMesh
bl_delay_refresh = 10
bl_delay_apply = 10
bl_automatic_push = True
bl_icon = 'MESH_DATA'

View File

@ -6,6 +6,13 @@ from .bl_datablock import BlDatablock
class BlMetaball(BlDatablock): class BlMetaball(BlDatablock):
bl_id = "metaballs"
bl_class = bpy.types.MetaBall
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'META_BALL'
def construct(self, data): def construct(self, data):
return bpy.data.metaballs.new(data["name"]) return bpy.data.metaballs.new(data["name"])
@ -17,7 +24,7 @@ class BlMetaball(BlDatablock):
new_element = target.elements.new(type=data["elements"][element]['type']) new_element = target.elements.new(type=data["elements"][element]['type'])
utils.dump_anything.load(new_element, data["elements"][element]) utils.dump_anything.load(new_element, data["elements"][element])
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
dumper = utils.dump_anything.Dumper() dumper = utils.dump_anything.Dumper()
dumper.depth = 3 dumper.depth = 3
@ -26,16 +33,5 @@ class BlMetaball(BlDatablock):
data = dumper.dump(pointer) data = dumper.dump(pointer)
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.metaballs)
def is_valid(self): def is_valid(self):
return bpy.data.metaballs.get(self.data['name']) return bpy.data.metaballs.get(self.data['name'])
bl_id = "metaballs"
bl_class = bpy.types.MetaBall
bl_rep_class = BlMetaball
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'META_BALL'

View File

@ -1,11 +1,40 @@
import bpy import bpy
import mathutils import mathutils
import logging
from .. import utils from .. import utils
from .bl_datablock import BlDatablock 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:
target.constraints.remove(local_constraint)
for constraint in data:
target_constraint = target.constraints.get(constraint)
if not target_constraint:
target_constraint = target.constraints.new(
data[constraint]['type'])
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): class BlObject(BlDatablock):
bl_id = "objects"
bl_class = bpy.types.Object
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'OBJECT_DATA'
def construct(self, data): def construct(self, data):
pointer = None pointer = None
@ -42,24 +71,26 @@ class BlObject(BlDatablock):
elif data["data"] in bpy.data.speakers.keys(): elif data["data"] in bpy.data.speakers.keys():
pointer = bpy.data.speakers[data["data"]] pointer = bpy.data.speakers[data["data"]]
elif data["data"] in bpy.data.lightprobes.keys(): elif data["data"] in bpy.data.lightprobes.keys():
pass # Only supported since 2.83
# bpy need to support correct lightprobe creation from python if bpy.app.version[1] >= 83:
# pointer = bpy.data.lightprobes[data["data"]] pointer = bpy.data.lightprobes[data["data"]]
else:
instance = bpy.data.objects.new(data["name"], pointer) logger.error("Lightprobe replication only supported since 2.83")
instance = bpy.data.objects.new(data["name"], pointer)
instance.uuid = self.uuid instance.uuid = self.uuid
return instance return instance
def load(self, data, target): def load_implementation(self, data, target):
target.matrix_world = mathutils.Matrix(data["matrix_world"]) if "matrix_world" in data:
target.matrix_world = mathutils.Matrix(data["matrix_world"])
target.name = data["name"] target.name = data["name"]
# Load modifiers # Load modifiers
if hasattr(target, 'modifiers'): if hasattr(target, 'modifiers'):
for local_modifier in target.modifiers: # TODO: smarter selective update
if local_modifier.name not in data['modifiers']: target.modifiers.clear()
target.modifiers.remove(local_modifier)
for modifier in data['modifiers']: for modifier in data['modifiers']:
target_modifier = target.modifiers.get(modifier) target_modifier = target.modifiers.get(modifier)
@ -70,6 +101,42 @@ class BlObject(BlDatablock):
utils.dump_anything.load( utils.dump_anything.load(
target_modifier, data['modifiers'][modifier]) target_modifier, data['modifiers'][modifier])
# Load constraints
# Object
if hasattr(target, 'constraints') and 'constraints' in data:
load_constraints(target, data['constraints'])
# Pose
if 'pose' in data:
if not target.pose:
raise Exception('No pose data yet (Fixed in a near futur)')
# Bone groups
for bg_name in data['pose']['bone_groups']:
bg_data = data['pose']['bone_groups'].get(bg_name)
bg_target = target.pose.bone_groups.get(bg_name)
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
# Bones
for bone in data['pose']['bones']:
target_bone = target.pose.bones.get(bone)
bone_data = data['pose']['bones'].get(bone)
if 'constraints' in bone_data.keys():
load_constraints(
target_bone, bone_data['constraints'])
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 # Load relations
if 'children' in data.keys(): if 'children' in data.keys():
for child in data['children']: for child in data['children']:
@ -80,17 +147,46 @@ class BlObject(BlDatablock):
target.empty_display_type = data['empty_display_type'] target.empty_display_type = data['empty_display_type']
# Instancing # Instancing
target.instance_type = data['instance_type'] target.instance_type = data['instance_type']
if data['instance_type'] == 'COLLECTION': if data['instance_type'] == 'COLLECTION':
target.instance_collection = bpy.data.collections[data['instance_collection']] target.instance_collection = bpy.data.collections[data['instance_collection']]
def dump(self, pointer=None): # vertex groups
if 'vertex_groups' in data:
target.vertex_groups.clear()
for vg in data['vertex_groups']:
vertex_group = target.vertex_groups.new(name=vg['name'])
for vert in vg['vertices']:
vertex_group.add(
[vert['index']], vert['weight'], 'REPLACE')
# SHAPE KEYS
if 'shape_keys' in data:
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)
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):
assert(pointer) assert(pointer)
dumper = utils.dump_anything.Dumper() dumper = utils.dump_anything.Dumper()
dumper.depth = 1 dumper.depth = 1
dumper.include_filter = [ dumper.include_filter = [
"name", "name",
"matrix_world",
"rotation_mode", "rotation_mode",
"parent", "parent",
"data", "data",
@ -101,6 +197,9 @@ class BlObject(BlDatablock):
"instance_collection", "instance_collection",
"instance_type" "instance_type"
] ]
# if not utils.has_action(pointer):
dumper.include_filter.append('matrix_world')
data = dumper.dump(pointer) data = dumper.dump(pointer)
if self.is_library: if self.is_library:
@ -109,8 +208,55 @@ class BlObject(BlDatablock):
# MODIFIERS # MODIFIERS
if hasattr(pointer, 'modifiers'): if hasattr(pointer, 'modifiers'):
dumper.include_filter = None dumper.include_filter = None
dumper.depth = 2
data["modifiers"] = {}
for index, modifier in enumerate(pointer.modifiers):
data["modifiers"][modifier.name] = dumper.dump(modifier)
data["modifiers"][modifier.name]['m_index'] = index
# CONSTRAINTS
# OBJECT
if hasattr(pointer, 'constraints'):
dumper.depth = 3 dumper.depth = 3
data["modifiers"] = dumper.dump(pointer.modifiers) data["constraints"] = dumper.dump(pointer.constraints)
# POSE
if hasattr(pointer, 'pose') and pointer.pose:
# BONES
bones = {}
for bone in pointer.pose.bones:
bones[bone.name] = {}
dumper.depth = 1
rotation = 'rotation_quaternion' if bone.rotation_mode == 'QUATERNION' else 'rotation_euler'
group_index = 'bone_group_index' if bone.bone_group else None
dumper.include_filter = [
'rotation_mode',
'location',
'scale',
group_index,
rotation
]
bones[bone.name] = dumper.dump(bone)
dumper.include_filter = []
dumper.depth = 3
bones[bone.name]["constraints"] = dumper.dump(bone.constraints)
data['pose'] = {'bones': bones}
# GROUPS
bone_groups = {}
for group in pointer.pose.bone_groups:
dumper.depth = 3
dumper.include_filter = [
'name',
'color_set'
]
bone_groups[group.name] = dumper.dump(group)
data['pose']['bone_groups'] = bone_groups
# CHILDS # CHILDS
if len(pointer.children) > 0: if len(pointer.children) > 0:
@ -120,13 +266,64 @@ class BlObject(BlDatablock):
data["children"] = childs data["children"] = childs
# VERTEx GROUP
if len(pointer.vertex_groups) > 0:
vg_data = []
for vg in pointer.vertex_groups:
vg_idx = vg.index
dumped_vg = {}
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:
dumper = utils.dump_anything.Dumper()
dumper.depth = 2
dumper.include_filter = [
'reference_key',
'use_relative'
]
data['shape_keys'] = dumper.dump(pointer_data.shape_keys)
data['shape_keys']['reference_key'] = pointer_data.shape_keys.reference_key.name
key_blocks = {}
for key in pointer_data.shape_keys.key_blocks:
dumper.depth = 3
dumper.include_filter = [
'name',
'data',
'mute',
'value',
'slider_min',
'slider_max',
'data',
'co'
]
key_blocks[key.name] = dumper.dump(key)
key_blocks[key.name]['relative_key'] = key.relative_key.name
data['shape_keys']['key_blocks'] = key_blocks
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.objects)
def resolve_dependencies(self): def resolve_dependencies(self):
deps = [] deps = super().resolve_dependencies()
# Avoid Empty case # Avoid Empty case
if self.pointer.data: if self.pointer.data:
@ -137,8 +334,9 @@ class BlObject(BlDatablock):
if self.is_library: if self.is_library:
deps.append(self.pointer.library) deps.append(self.pointer.library)
if self.pointer.instance_type == 'COLLECTION': if self.pointer.instance_type == 'COLLECTION':
#TODO: uuid based # TODO: uuid based
deps.append(self.pointer.instance_collection) deps.append(self.pointer.instance_collection)
return deps return deps
@ -147,10 +345,3 @@ class BlObject(BlDatablock):
return bpy.data.objects.get(self.data['name']) return bpy.data.objects.get(self.data['name'])
bl_id = "objects"
bl_class = bpy.types.Object
bl_rep_class = BlObject
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'OBJECT_DATA'

View File

@ -5,6 +5,13 @@ from .. import utils
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
class BlScene(BlDatablock): class BlScene(BlDatablock):
bl_id = "scenes"
bl_class = bpy.types.Scene
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'SCENE_DATA'
def construct(self, data): def construct(self, data):
instance = bpy.data.scenes.new(data["name"]) instance = bpy.data.scenes.new(data["name"])
instance.uuid = self.uuid instance.uuid = self.uuid
@ -42,7 +49,7 @@ class BlScene(BlDatablock):
if 'grease_pencil' in data.keys(): if 'grease_pencil' in data.keys():
target.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']] target.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']]
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
data = {} data = {}
@ -58,12 +65,6 @@ class BlScene(BlDatablock):
return data return data
def resolve(self):
scene_name = self.data['name']
self.pointer = bpy.data.scenes.get(scene_name)
# self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.objects)
def resolve_dependencies(self): def resolve_dependencies(self):
deps = [] deps = []
@ -87,10 +88,3 @@ class BlScene(BlDatablock):
def is_valid(self): def is_valid(self):
return bpy.data.scenes.get(self.data['name']) return bpy.data.scenes.get(self.data['name'])
bl_id = "scenes"
bl_class = bpy.types.Scene
bl_rep_class = BlScene
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'SCENE_DATA'

View File

@ -6,6 +6,13 @@ from .bl_datablock import BlDatablock
class BlSpeaker(BlDatablock): class BlSpeaker(BlDatablock):
bl_id = "speakers"
bl_class = bpy.types.Speaker
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'SPEAKER'
def load(self, data, target): def load(self, data, target):
utils.dump_anything.load(target, data) utils.dump_anything.load(target, data)
@ -34,17 +41,6 @@ class BlSpeaker(BlDatablock):
return dumper.dump(pointer) return dumper.dump(pointer)
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lattices)
def is_valid(self): def is_valid(self):
return bpy.data.lattices.get(self.data['name']) return bpy.data.lattices.get(self.data['name'])
bl_id = "speakers"
bl_class = bpy.types.Speaker
bl_rep_class = BlSpeaker
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'SPEAKER'

View File

@ -1,65 +0,0 @@
import bpy
import mathutils
import jsondiff
from .. import utils
from .. import presence
from .bl_datablock import BlDatablock
from ..libs.replication.replication.constants import UP
class BlUser(BlDatablock):
def construct(self, name):
return presence.User()
def load(self, data, target):
target.name = data['name']
target.location = data['location']
target.selected_objects = data['selected_objects']
utils.dump_anything.load(target, data)
def apply(self):
if self.pointer:
self.load(data=self.data, target=self.pointer)
presence.refresh_3d_view()
self.state = UP
def dump(self,pointer=None):
data = utils.dump_anything.dump(pointer)
data['location'] = pointer.location
data['color'] = pointer.color
data['selected_objects'] = pointer.selected_objects
data['view_matrix'] = pointer.view_matrix
return data
def update(self):
self.pointer.is_dirty = True
# def diff(self):
# if not self.pointer:
# return False
# if self.pointer.is_dirty:
# self.pointer.is_dirty = False
# return True
# for i,coord in enumerate(self.pointer.location):
# if coord != self.data['location'][i]:
# return True
# return False
def is_valid(self):
return True
bl_id = "users"
bl_class = presence.User
bl_rep_class = BlUser
bl_delay_refresh = .1
bl_delay_apply = .1
bl_automatic_push = True
bl_icon = 'CON_ARMATURE'

View File

@ -7,6 +7,13 @@ from .bl_material import load_link, load_node
class BlWorld(BlDatablock): class BlWorld(BlDatablock):
bl_id = "worlds"
bl_class = bpy.types.World
bl_delay_refresh = 4
bl_delay_apply = 4
bl_automatic_push = True
bl_icon = 'WORLD_DATA'
def construct(self, data): def construct(self, data):
return bpy.data.worlds.new(data["name"]) return bpy.data.worlds.new(data["name"])
@ -26,7 +33,7 @@ class BlWorld(BlDatablock):
for link in data["node_tree"]["links"]: for link in data["node_tree"]["links"]:
load_link(target.node_tree, data["node_tree"]["links"][link]) load_link(target.node_tree, data["node_tree"]["links"][link])
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
world_dumper = utils.dump_anything.Dumper() world_dumper = utils.dump_anything.Dumper()
@ -83,9 +90,6 @@ class BlWorld(BlDatablock):
pointer.node_tree, ["links"], 3, data['node_tree']) pointer.node_tree, ["links"], 3, data['node_tree'])
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.worlds)
def resolve_dependencies(self): def resolve_dependencies(self):
deps = [] deps = []
@ -100,11 +104,3 @@ class BlWorld(BlDatablock):
def is_valid(self): def is_valid(self):
return bpy.data.worlds.get(self.data['name']) return bpy.data.worlds.get(self.data['name'])
bl_id = "worlds"
bl_class = bpy.types.World
bl_rep_class = BlWorld
bl_delay_refresh = 4
bl_delay_apply = 4
bl_automatic_push = True
bl_icon = 'WORLD_DATA'

View File

@ -3,10 +3,10 @@ import logging
import bpy import bpy
from . import operators, presence, utils from . import operators, presence, utils
from .bl_types.bl_user import BlUser
from .libs.replication.replication.constants import FETCHED, RP_COMMON from .libs.replication.replication.constants import FETCHED, RP_COMMON
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class Delayable(): class Delayable():
@ -74,74 +74,101 @@ class ApplyTimer(Timer):
try: try:
operators.client.apply(node) operators.client.apply(node)
except Exception as e: except Exception as e:
logger.error("fail to apply {}: {}".format(node_ref.uuid,e)) logger.error(
"fail to apply {}: {}".format(node_ref.uuid, e))
class DynamicRightSelectTimer(Timer): class DynamicRightSelectTimer(Timer):
def __init__(self, timout=.1): def __init__(self, timout=.1):
super().__init__(timout) super().__init__(timout)
self.last_selection = [] self._last_selection = []
self._user = None
self._right_strategy = RP_COMMON
def execute(self): def execute(self):
if operators.client: session = operators.client
users = operators.client.list(filter=BlUser) settings = bpy.context.window_manager.session
for user in users: if session:
user_ref = operators.client.get(uuid=user) # Find user
settings = bpy.context.window_manager.session if self._user is None:
self._user = session.online_users.get(settings.username)
# Local user if self._right_strategy is None:
if user_ref.pointer: self._right_strategy = session.get_config()[
current_selection = utils.get_selected_objects( 'right_strategy']
bpy.context.scene)
if current_selection != self.last_selection:
right_strategy = operators.client.get_config()[
'right_strategy']
if right_strategy == RP_COMMON:
obj_common = [
o for o in self.last_selection if o not in current_selection]
obj_ours = [
o for o in current_selection if o not in self.last_selection]
# change new selection to our if self._user:
for obj in obj_ours: current_selection = utils.get_selected_objects(
node = operators.client.get(uuid=obj) bpy.context.scene)
if current_selection != self._last_selection:
if self._right_strategy == RP_COMMON:
obj_common = [
o for o in self._last_selection if o not in current_selection]
obj_ours = [
o for o in current_selection if o not in self._last_selection]
if node and node.owner == RP_COMMON: # change old selection right to common
recursive = True for obj in obj_common:
if node.data and 'instance_type' in node.data.keys(): node = session.get(uuid=obj)
recursive = node.data['instance_type'] != 'COLLECTION'
operators.client.change_owner( if node and (node.owner == settings.username or node.owner == RP_COMMON):
node.uuid, recursive = True
settings.username, if node.data and 'instance_type' in node.data.keys():
recursive=recursive) recursive = node.data['instance_type'] != 'COLLECTION'
else: session.change_owner(
return node.uuid,
RP_COMMON,
recursive=recursive)
self.last_selection = current_selection # change new selection to our
user_ref.pointer.update_selected_objects( for obj in obj_ours:
bpy.context) node = session.get(uuid=obj)
user_ref.update()
# change old selection right to common if node and node.owner == RP_COMMON:
for obj in obj_common: recursive = True
node = operators.client.get(uuid=obj) if node.data and 'instance_type' in node.data.keys():
recursive = node.data['instance_type'] != 'COLLECTION'
if node and (node.owner == settings.username or node.owner == RP_COMMON): session.change_owner(
recursive = True node.uuid,
if node.data and 'instance_type' in node.data.keys(): settings.username,
recursive = node.data['instance_type'] != 'COLLECTION' recursive=recursive)
operators.client.change_owner( else:
node.uuid, return
RP_COMMON,
recursive=recursive) self._last_selection = current_selection
else:
for obj in bpy.data.objects: user_metadata = {
if obj.hide_select and obj.uuid not in user_ref.data['selected_objects']: 'selected_objects': current_selection
obj.hide_select = False }
elif not obj.hide_select and obj.uuid in user_ref.data['selected_objects']:
obj.hide_select = True session.update_user_metadata(user_metadata)
logger.info("Update selection")
# Fix deselection until right managment refactoring (with Roles concepts)
if len(current_selection) == 0 and self._right_strategy == RP_COMMON:
owned_keys = session.list(
filter_owner=settings.username)
for key in owned_keys:
node = session.get(uuid=key)
session.change_owner(
key,
RP_COMMON,
recursive=recursive)
for user, user_info in session.online_users.items():
if user != settings.username:
metadata = user_info.get('metadata')
if 'selected_objects' in metadata:
# Update selectionnable objects
for obj in bpy.data.objects:
if obj.hide_select and obj.uuid not in metadata['selected_objects']:
obj.hide_select = False
elif not obj.hide_select and obj.uuid in metadata['selected_objects']:
obj.hide_select = True
class Draw(Delayable): class Draw(Delayable):
@ -160,37 +187,89 @@ class Draw(Delayable):
bpy.types.SpaceView3D.draw_handler_remove( bpy.types.SpaceView3D.draw_handler_remove(
self._handler, "WINDOW") self._handler, "WINDOW")
except: except:
logger.error("draw already unregistered") pass
class DrawClient(Draw): class DrawClient(Draw):
def execute(self): def execute(self):
repo = operators.client session = getattr(operators, 'client', None)
if repo and presence.renderer: renderer = getattr(presence, 'renderer', None)
settings = bpy.context.window_manager.session
client_list = [key for key in repo.list(filter=BlUser) if
key != settings.user_uuid]
for cli in client_list: if session and renderer:
cli_ref = repo.get(uuid=cli) settings = bpy.context.window_manager.session
if cli_ref.data.get('name'): users = session.online_users
if settings.presence_show_selected:
presence.renderer.draw_client_selection( for user in users.values():
cli_ref.data['name'], cli_ref.data['color'], cli_ref.data['selected_objects']) metadata = user.get('metadata')
if settings.presence_show_user:
presence.renderer.draw_client_camera( if settings.presence_show_selected and 'selected_objects' in metadata.keys():
cli_ref.data['name'], cli_ref.data['location'], cli_ref.data['color']) renderer.draw_client_selection(
user['id'], metadata['color'], metadata['selected_objects'])
if settings.presence_show_user and 'view_corners' in metadata:
renderer.draw_client_camera(
user['id'], metadata['view_corners'], metadata['color'])
class ClientUpdate(Timer): class ClientUpdate(Timer):
def __init__(self, timout=1, client_uuid=None): def __init__(self, timout=1):
assert(client_uuid)
self._client_uuid = client_uuid
super().__init__(timout) super().__init__(timout)
def execute(self): def execute(self):
if self._client_uuid and operators.client: settings = bpy.context.window_manager.session
client = operators.client.get(uuid=self._client_uuid) session_info = bpy.context.window_manager.session
session = getattr(operators, 'client', None)
renderer = getattr(presence, 'renderer', None)
if client: if session and renderer:
client.pointer.update_location() # Check if session has been closes prematurely
if session.state == 0:
bpy.ops.session.stop()
local_user = operators.client.online_users.get(
session_info.username)
if not local_user:
return
local_user_metadata = local_user.get('metadata')
current_view_corners = presence.get_view_corners()
if not local_user_metadata:
logger.info("init user metadata")
metadata = {
'view_corners': current_view_corners,
'view_matrix': presence.get_view_matrix(),
'color': (settings.client_color.r,
settings.client_color.g,
settings.client_color.b,
1),
'frame_current':bpy.context.scene.frame_current
}
session.update_user_metadata(metadata)
elif current_view_corners != local_user_metadata['view_corners']:
logger.info('update user metadata')
local_user_metadata['view_corners'] = current_view_corners
local_user_metadata['view_matrix'] = presence.get_view_matrix()
session.update_user_metadata(local_user_metadata)
# sync online users
session_users = operators.client.online_users
ui_users = bpy.context.window_manager.online_users
for index, user in enumerate(ui_users):
if user.username not in session_users.keys():
ui_users.remove(index)
renderer.flush_selection()
renderer.flush_users()
break
for user in session_users:
if user not in ui_users:
new_key = ui_users.add()
new_key.name = user
new_key.username = user
# TODO: event drivent 3d view refresh
presence.refresh_3d_view()

View File

@ -6,7 +6,7 @@ import sys
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR) logger.setLevel(logging.INFO)
CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config") CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config")
CONFIG = os.path.join(CONFIG_DIR, "app.yaml") CONFIG = os.path.join(CONFIG_DIR, "app.yaml")

View File

@ -92,11 +92,13 @@ class Dumper:
def _build_inline_dump_functions(self): def _build_inline_dump_functions(self):
self._dump_identity = (lambda x, depth: x, lambda x, depth: x) self._dump_identity = (lambda x, depth: x, lambda x, depth: x)
self._dump_ref = (lambda x, depth: x.name, self._dump_object_as_branch)
self._dump_ID = (lambda x, depth: x.name, self._dump_default_as_branch) self._dump_ID = (lambda x, depth: x.name, self._dump_default_as_branch)
self._dump_collection = (self._dump_default_as_leaf, self._dump_collection_as_branch) self._dump_collection = (self._dump_default_as_leaf, self._dump_collection_as_branch)
self._dump_array = (self._dump_default_as_leaf, self._dump_array_as_branch) self._dump_array = (self._dump_default_as_leaf, self._dump_array_as_branch)
self._dump_matrix = (self._dump_matrix_as_leaf, self._dump_matrix_as_leaf) self._dump_matrix = (self._dump_matrix_as_leaf, self._dump_matrix_as_leaf)
self._dump_vector = (self._dump_vector_as_leaf, self._dump_vector_as_leaf) self._dump_vector = (self._dump_vector_as_leaf, self._dump_vector_as_leaf)
self._dump_quaternion = (self._dump_quaternion_as_leaf, self._dump_quaternion_as_leaf)
self._dump_default = (self._dump_default_as_leaf, self._dump_default_as_branch) self._dump_default = (self._dump_default_as_leaf, self._dump_default_as_branch)
self._dump_color = (self._dump_color_as_leaf, self._dump_color_as_leaf) self._dump_color = (self._dump_color_as_leaf, self._dump_color_as_leaf)
@ -105,11 +107,13 @@ class Dumper:
self._match_type_int = (_dump_filter_type(int), self._dump_identity) self._match_type_int = (_dump_filter_type(int), self._dump_identity)
self._match_type_float = (_dump_filter_type(float), self._dump_identity) self._match_type_float = (_dump_filter_type(float), self._dump_identity)
self._match_type_string = (_dump_filter_type(str), self._dump_identity) self._match_type_string = (_dump_filter_type(str), self._dump_identity)
self._match_type_ref = (_dump_filter_type(T.Object), self._dump_ref)
self._match_type_ID = (_dump_filter_type(T.ID), self._dump_ID) self._match_type_ID = (_dump_filter_type(T.ID), self._dump_ID)
self._match_type_bpy_prop_collection = (_dump_filter_type(T.bpy_prop_collection), self._dump_collection) self._match_type_bpy_prop_collection = (_dump_filter_type(T.bpy_prop_collection), self._dump_collection)
self._match_type_array = (_dump_filter_array, self._dump_array) self._match_type_array = (_dump_filter_array, self._dump_array)
self._match_type_matrix = (_dump_filter_type(mathutils.Matrix), self._dump_matrix) 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_vector = (_dump_filter_type(mathutils.Vector), self._dump_vector)
self._match_type_quaternion = (_dump_filter_type(mathutils.Quaternion), self._dump_quaternion)
self._match_type_color = (_dump_filter_type_by_name("Color"), self._dump_color) self._match_type_color = (_dump_filter_type_by_name("Color"), self._dump_color)
self._match_default = (_dump_filter_default, self._dump_default) self._match_default = (_dump_filter_default, self._dump_default)
@ -136,9 +140,18 @@ class Dumper:
def _dump_vector_as_leaf(self, vector, depth): def _dump_vector_as_leaf(self, vector, depth):
return list(vector) return list(vector)
def _dump_quaternion_as_leaf(self, quaternion, depth):
return list(quaternion)
def _dump_color_as_leaf(self, color, depth): def _dump_color_as_leaf(self, color, depth):
return list(color) return list(color)
def _dump_object_as_branch(self, default, depth):
if depth == 1:
return self._dump_default_as_branch(default, depth)
else:
return default.name
def _dump_default_as_branch(self, default, depth): def _dump_default_as_branch(self, default, depth):
def is_valid_property(p): def is_valid_property(p):
try: try:
@ -173,12 +186,13 @@ class Dumper:
self._match_type_int, self._match_type_int,
self._match_type_float, self._match_type_float,
self._match_type_string, self._match_type_string,
self._match_type_ref,
self._match_type_ID, self._match_type_ID,
self._match_type_bpy_prop_collection, self._match_type_bpy_prop_collection,
self._match_type_array, self._match_type_array,
self._match_type_matrix, self._match_type_matrix,
self._match_type_vector, self._match_type_vector,
self._match_type_color, self._match_type_quaternion,
self._match_type_color, self._match_type_color,
self._match_default self._match_default
] ]
@ -307,6 +321,9 @@ class Loader:
def _load_vector(self, vector, dump): def _load_vector(self, vector, dump):
vector.write(mathutils.Vector(dump)) vector.write(mathutils.Vector(dump))
def _load_quaternion(self, quaternion, dump):
quaternion.write(mathutils.Quaternion(dump))
def _ordered_keys(self, keys): def _ordered_keys(self, keys):
ordered_keys = [] ordered_keys = []
for order_element in self.order: for order_element in self.order:
@ -336,6 +353,7 @@ class Loader:
(_load_filter_type(T.IntProperty), self._load_identity), (_load_filter_type(T.IntProperty), self._load_identity),
(_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.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.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(T.FloatProperty), self._load_identity), (_load_filter_type(T.FloatProperty), self._load_identity),
(_load_filter_type(T.StringProperty), self._load_identity), (_load_filter_type(T.StringProperty), self._load_identity),
(_load_filter_type(T.EnumProperty), self._load_identity), (_load_filter_type(T.EnumProperty), self._load_identity),

View File

@ -8,7 +8,6 @@ import subprocess
import time import time
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
import msgpack import msgpack
import bpy import bpy
@ -23,34 +22,25 @@ from .libs.replication.replication.interface import Session
from .libs.replication.replication.constants import ( from .libs.replication.replication.constants import (
STATE_ACTIVE, STATE_ACTIVE,
STATE_INITIAL, STATE_INITIAL,
STATE_SYNCING) STATE_SYNCING,
FETCHED)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR) logger.setLevel(logging.INFO)
client = None client = None
delayables = [] delayables = []
ui_context = None ui_context = None
stop_modal_executor = False
modal_executor_queue = None
def init_supported_datablocks(supported_types_id):
global client
for type_id in supported_types_id:
if hasattr(bpy.data, type_id):
for item in getattr(bpy.data, type_id):
if client.exist(uuid=item.uuid):
continue
else:
client.add(item)
# OPERATORS # OPERATORS
class SessionStartOperator(bpy.types.Operator): class SessionStartOperator(bpy.types.Operator):
bl_idname = "session.start" bl_idname = "session.start"
bl_label = "start" bl_label = "start"
bl_description = "connect to a net server" bl_description = "connect to a net server"
bl_options = {"REGISTER"}
host: bpy.props.BoolProperty(default=False) host: bpy.props.BoolProperty(default=False)
@ -59,32 +49,41 @@ class SessionStartOperator(bpy.types.Operator):
return True return True
def execute(self, context): def execute(self, context):
global client, delayables global client, delayables, ui_context
settings = context.window_manager.session settings = context.window_manager.session
users = bpy.data.window_managers['WinMan'].online_users
# TODO: Sync server clients
users.clear()
# save config # save config
settings.save(context) settings.save(context)
bpy_factory = ReplicatedDataFactory() bpy_factory = ReplicatedDataFactory()
supported_bl_types = [] supported_bl_types = []
ui_context = context.copy()
# init the factory with supported types # init the factory with supported types
for type in bl_types.types_to_register(): for type in bl_types.types_to_register():
_type = getattr(bl_types, type) type_module = getattr(bl_types, type)
supported_bl_types.append(_type.bl_id) type_impl_name = "Bl{}".format(type.split('_')[1].capitalize())
type_module_class = getattr(type_module, type_impl_name)
supported_bl_types.append(type_module_class.bl_id)
# Retreive local replicated types settings # Retreive local replicated types settings
type_local_config = settings.supported_datablock[_type.bl_rep_class.__name__] type_local_config = settings.supported_datablock[type_impl_name]
bpy_factory.register_type( bpy_factory.register_type(
_type.bl_class, type_module_class.bl_class,
_type.bl_rep_class, type_module_class,
timer=type_local_config.bl_delay_refresh, timer=type_local_config.bl_delay_refresh,
automatic=type_local_config.auto_push) automatic=type_local_config.auto_push)
if type_local_config.bl_delay_apply > 0: if type_local_config.bl_delay_apply > 0:
delayables.append(delayable.ApplyTimer( delayables.append(delayable.ApplyTimer(
timout=type_local_config.bl_delay_apply, timout=type_local_config.bl_delay_apply,
target_type=_type.bl_rep_class)) target_type=type_module_class))
client = Session(factory=bpy_factory) client = Session(factory=bpy_factory)
@ -118,30 +117,15 @@ class SessionStartOperator(bpy.types.Operator):
"A session is already hosted on this address") "A session is already hosted on this address")
return {"CANCELLED"} return {"CANCELLED"}
if self.host:
# Init user settings
usr = presence.User(
username=settings.username,
color=(settings.client_color.r,
settings.client_color.g,
settings.client_color.b,
1),
)
settings.user_uuid = client.add(usr,owner=settings.username)
client.commit(settings.user_uuid)
if settings.init_scene and self.host:
for scene in bpy.data.scenes: for scene in bpy.data.scenes:
scene_uuid = client.add(scene) scene_uuid = client.add(scene)
# for node in client.list(): # for node in client.list():
client.commit(scene_uuid) client.commit(scene_uuid)
delayables.append(delayable.ClientUpdate(
client_uuid=settings.user_uuid))
delayables.append(delayable.DrawClient())
delayables.append(delayable.ClientUpdate())
delayables.append(delayable.DrawClient())
delayables.append(delayable.DynamicRightSelectTimer()) delayables.append(delayable.DynamicRightSelectTimer())
# Push all added values # Push all added values
@ -155,6 +139,10 @@ class SessionStartOperator(bpy.types.Operator):
for d in delayables: for d in delayables:
d.register() d.register()
global modal_executor_queue
modal_executor_queue = queue.Queue()
bpy.ops.session.apply_armature_operator()
self.report( self.report(
{'INFO'}, {'INFO'},
"connexion on tcp://{}:{}".format(settings.ip, settings.port)) "connexion on tcp://{}:{}".format(settings.ip, settings.port))
@ -172,12 +160,13 @@ class SessionStopOperator(bpy.types.Operator):
return True return True
def execute(self, context): def execute(self, context):
global client, delayables global client, delayables, stop_modal_executor
stop_modal_executor = True
settings = context.window_manager.session settings = context.window_manager.session
settings.is_admin = False settings.is_admin = False
assert(client) assert(client)
client.remove(settings.user_uuid)
client.disconnect() client.disconnect()
for d in delayables: for d in delayables:
@ -264,6 +253,14 @@ class SessionSnapUserOperator(bpy.types.Operator):
def execute(self, context): def execute(self, context):
wm = context.window_manager wm = context.window_manager
settings = context.window_manager.session
if settings.time_snap_running:
settings.time_snap_running = False
return {'CANCELLED'}
else:
settings.time_snap_running = True
self._timer = wm.event_timer_add(0.1, window=context.window) self._timer = wm.event_timer_add(0.1, window=context.window)
wm.modal_handler_add(self) wm.modal_handler_add(self)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
@ -273,7 +270,9 @@ class SessionSnapUserOperator(bpy.types.Operator):
wm.event_timer_remove(self._timer) wm.event_timer_remove(self._timer)
def modal(self, context, event): def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}: is_running = context.window_manager.session.time_snap_running
if event.type in {'RIGHTMOUSE', 'ESC'} or not is_running:
self.cancel(context) self.cancel(context)
return {'CANCELLED'} return {'CANCELLED'}
@ -281,9 +280,64 @@ class SessionSnapUserOperator(bpy.types.Operator):
area, region, rv3d = presence.view3d_find() area, region, rv3d = presence.view3d_find()
global client global client
target_client = client.get(uuid=self.target_client) if client:
if target_client: target_ref = client.online_users.get(self.target_client)
rv3d.view_matrix = mathutils.Matrix(target_client.data['view_matrix'])
if target_ref:
rv3d.view_matrix = mathutils.Matrix(
target_ref['metadata']['view_matrix'])
else:
return {"CANCELLED"}
return {'PASS_THROUGH'}
class SessionSnapTimeOperator(bpy.types.Operator):
bl_idname = "session.snaptime"
bl_label = "snap to user time"
bl_description = "Snap time to selected user time's"
bl_options = {"REGISTER"}
_timer = None
target_client: bpy.props.StringProperty(default="None")
@classmethod
def poll(cls, context):
return True
def execute(self, context):
settings = context.window_manager.session
if settings.user_snap_running:
settings.user_snap_running = False
return {'CANCELLED'}
else:
settings.user_snap_running = True
wm = context.window_manager
self._timer = wm.event_timer_add(0.1, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
def modal(self, context, event):
is_running = context.window_manager.session.user_snap_running
if event.type in {'RIGHTMOUSE', 'ESC'} or not is_running:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
global client
if client:
target_ref = client.online_users.get(self.target_client)
if target_ref:
context.scene.frame_current = target_ref['metadata']['frame_current']
else: else:
return {"CANCELLED"} return {"CANCELLED"}
@ -330,15 +384,65 @@ class SessionCommit(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class ApplyArmatureOperator(bpy.types.Operator):
"""Operator which runs its self from a timer"""
bl_idname = "session.apply_armature_operator"
bl_label = "Modal Executor Operator"
_timer = None
def modal(self, context, event):
global stop_modal_executor, modal_executor_queue
if stop_modal_executor:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
global client
nodes = client.list(filter=bl_types.bl_armature.BlArmature)
for node in nodes:
node_ref = client.get(uuid=node)
if node_ref.state == FETCHED:
try:
client.apply(node)
except Exception as e:
logger.error(
"fail to apply {}: {}".format(node_ref.uuid, e))
return {'PASS_THROUGH'}
def execute(self, context):
wm = context.window_manager
self._timer = wm.event_timer_add(2, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
global stop_modal_executor
wm = context.window_manager
wm.event_timer_remove(self._timer)
stop_modal_executor = False
classes = ( classes = (
SessionStartOperator, SessionStartOperator,
SessionStopOperator, SessionStopOperator,
SessionPropertyRemoveOperator, SessionPropertyRemoveOperator,
SessionSnapUserOperator, SessionSnapUserOperator,
SessionSnapTimeOperator,
SessionPropertyRightOperator, SessionPropertyRightOperator,
SessionApply, SessionApply,
SessionCommit, SessionCommit,
ApplyArmatureOperator,
) )
@persistent @persistent
def load_pre_handler(dummy): def load_pre_handler(dummy):
global client global client
@ -347,6 +451,29 @@ def load_pre_handler(dummy):
bpy.ops.session.stop() bpy.ops.session.stop()
@persistent
def sanitize_deps_graph(dummy):
"""sanitize deps graph
Temporary solution to resolve each node pointers after a Undo.
A future solution should be to avoid storing dataclock reference...
"""
global client
if client and client.state in [STATE_ACTIVE]:
for node_key in client.list():
client.get(node_key).resolve()
@persistent
def update_client_frame(scene):
if client and client.state == STATE_ACTIVE:
client.update_user_metadata({
'frame_current': scene.frame_current
})
def register(): def register():
from bpy.utils import register_class from bpy.utils import register_class
for cls in classes: for cls in classes:
@ -354,6 +481,12 @@ def register():
bpy.app.handlers.load_pre.append(load_pre_handler) bpy.app.handlers.load_pre.append(load_pre_handler)
bpy.app.handlers.undo_post.append(sanitize_deps_graph)
bpy.app.handlers.redo_post.append(sanitize_deps_graph)
bpy.app.handlers.frame_change_pre.append(update_client_frame)
def unregister(): def unregister():
global client global client
@ -367,5 +500,11 @@ def unregister():
bpy.app.handlers.load_pre.remove(load_pre_handler) bpy.app.handlers.load_pre.remove(load_pre_handler)
bpy.app.handlers.undo_post.remove(sanitize_deps_graph)
bpy.app.handlers.redo_post.remove(sanitize_deps_graph)
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View File

@ -31,8 +31,8 @@ def view3d_find():
def refresh_3d_view(): def refresh_3d_view():
area, region, rv3d = view3d_find() area, region, rv3d = view3d_find()
if area and region and rv3d:
area.tag_redraw() area.tag_redraw()
def get_target(region, rv3d, coord): def get_target(region, rv3d, coord):
@ -69,7 +69,7 @@ def get_default_bbox(obj, radius):
return [(point.x, point.y, point.z) return [(point.x, point.y, point.z)
for point in bbox_corners] for point in bbox_corners]
def get_client_cam_points(): def get_view_corners():
area, region, rv3d = view3d_find() area, region, rv3d = view3d_find()
v1 = [0, 0, 0] v1 = [0, 0, 0]
@ -78,6 +78,7 @@ def get_client_cam_points():
v4 = [0, 0, 0] v4 = [0, 0, 0]
v5 = [0, 0, 0] v5 = [0, 0, 0]
v6 = [0, 0, 0] v6 = [0, 0, 0]
v7 = [0, 0, 0]
if area and region and rv3d: if area and region and rv3d:
width = region.width width = region.width
@ -112,31 +113,14 @@ def get_bb_coords_from_obj(object, parent=None):
return [(point.x, point.y, point.z) return [(point.x, point.y, point.z)
for point in bbox_corners] for point in bbox_corners]
class User():
def __init__(self, username=None, color=(0, 0, 0, 1)):
self.is_dirty = False
self.name = username
self.color = color
self.location = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
self.selected_objects = []
self.last_select_objects = []
self.view_matrix = None
def update_location(self): def get_view_matrix():
current_coords = get_client_cam_points() area, region, rv3d = view3d_find()
area, region, rv3d = view3d_find()
current_coords = list(get_client_cam_points())
if current_coords and self.location != current_coords:
self.location = current_coords
if area and region and rv3d:
matrix_dumper = utils.dump_anything.Dumper() matrix_dumper = utils.dump_anything.Dumper()
current_vm = matrix_dumper.dump(rv3d.view_matrix)
if self.view_matrix != current_vm:
self.view_matrix = current_vm
def update_selected_objects(self, context): return matrix_dumper.dump(rv3d.view_matrix)
self.selected_objects = utils.get_selected_objects(context.scene)
def update_presence(self, context): def update_presence(self, context):
global renderer global renderer
@ -171,8 +155,12 @@ class DrawFactory(object):
self.register_handlers() self.register_handlers()
def stop(self): def stop(self):
self.flush_users()
self.flush_selection()
self.unregister_handlers() self.unregister_handlers()
refresh_3d_view()
def register_handlers(self): def register_handlers(self):
self.draw3d_handle = bpy.types.SpaceView3D.draw_handler_add( self.draw3d_handle = bpy.types.SpaceView3D.draw_handler_add(
self.draw3d_callback, (), 'WINDOW', 'POST_VIEW') self.draw3d_callback, (), 'WINDOW', 'POST_VIEW')
@ -215,14 +203,14 @@ class DrawFactory(object):
self.d2d_items.clear() self.d2d_items.clear()
def draw_client_selection(self, client_uuid, client_color, client_selection): def draw_client_selection(self, client_id, client_color, client_selection):
local_user = bpy.context.window_manager.session.user_uuid local_user = bpy.context.window_manager.session.username
if local_user != client_uuid: if local_user != client_id:
self.flush_selection(client_uuid) self.flush_selection(client_id)
for select_ob in client_selection: for select_ob in client_selection:
drawable_key = "{}_select_{}".format(client_uuid, select_ob) drawable_key = "{}_select_{}".format(client_id, select_ob)
ob = utils.find_from_attr("uuid", select_ob, bpy.data.objects) ob = utils.find_from_attr("uuid", select_ob, bpy.data.objects)
if not ob: if not ob:
@ -271,11 +259,11 @@ class DrawFactory(object):
self.d3d_items[key] = (shader, batch, color) self.d3d_items[key] = (shader, batch, color)
def draw_client_camera(self, client_uuid, client_location, client_color): def draw_client_camera(self, client_id, client_location, client_color):
if client_location: if client_location:
local_user = bpy.context.window_manager.session.user_uuid local_user = bpy.context.window_manager.session.username
if local_user != client_uuid: if local_user != client_id:
try: try:
indices = ( indices = (
(1, 3), (2, 1), (3, 0), (1, 3), (2, 1), (3, 0),
@ -290,8 +278,8 @@ class DrawFactory(object):
batch = batch_for_shader( batch = batch_for_shader(
shader, 'LINES', {"pos": position}, indices=indices) shader, 'LINES', {"pos": position}, indices=indices)
self.d3d_items[client_uuid] = (shader, batch, color) self.d3d_items[client_id] = (shader, batch, color)
self.d2d_items[client_uuid] = (position[1], client_uuid, color) self.d2d_items[client_id] = (position[1], client_id, color)
except Exception as e: except Exception as e:
logger.error("Draw client exception {}".format(e)) logger.error("Draw client exception {}".format(e))

View File

@ -1,7 +1,6 @@
import bpy import bpy
from . import operators from . import operators
from .bl_types.bl_user import BlUser
from .libs.replication.replication.constants import (ADDED, ERROR, FETCHED, from .libs.replication.replication.constants import (ADDED, ERROR, FETCHED,
MODIFIED, RP_COMMON, UP) MODIFIED, RP_COMMON, UP)
@ -167,7 +166,7 @@ class SESSION_PT_settings_replication(bpy.types.Panel):
class SESSION_PT_user(bpy.types.Panel): class SESSION_PT_user(bpy.types.Panel):
bl_idname = "MULTIUSER_USER_PT_panel" bl_idname = "MULTIUSER_USER_PT_panel"
bl_label = "Users" bl_label = "Online users"
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = "Multiuser" bl_category = "Multiuser"
@ -179,44 +178,58 @@ class SESSION_PT_user(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
online_users = context.window_manager.online_users
selected_user = context.window_manager.user_index
settings = context.window_manager.session settings = context.window_manager.session
active_user = online_users[selected_user] if len(online_users)-1>=selected_user else 0
# Create a simple row. # Create a simple row.
col = layout.column(align=True) row = layout.row()
box = row.box()
client_keys = operators.client.list(filter=BlUser) split = box.split(factor=0.5)
if client_keys and len(client_keys) > 0: split.label(text="user")
for key in client_keys: split.label(text="frame")
area_msg = col.row(align=True) split.label(text="ping")
item_box = area_msg.box()
client = operators.client.get(uuid=key).data
info = ""
detail_item_row = item_box.row(align=True)
if client.get('name'):
username = client['name']
is_local_user = username == settings.username
if is_local_user:
info = "(self)"
detail_item_row.label(
text="{} {}".format(username, info))
if not is_local_user:
detail_item_row.operator(
"session.snapview",
text="",
icon='VIEW_CAMERA').target_client = key
row = layout.row()
else:
row.label(text="Empty")
row = layout.row() row = layout.row()
layout.template_list("SESSION_UL_users", "", context.window_manager, "online_users", context.window_manager, "user_index")
if active_user != 0 and active_user.username != settings.username:
row = layout.row()
user_operations = row.split()
user_operations.alert = context.window_manager.session.time_snap_running
user_operations.operator(
"session.snapview",
text="",
icon='VIEW_CAMERA').target_client = active_user.username
user_operations.alert = context.window_manager.session.user_snap_running
user_operations.operator(
"session.snaptime",
text="",
icon='TIME').target_client = active_user.username
class SESSION_UL_users(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
session = operators.client
settings = context.window_manager.session
is_local_user = item.username == settings.username
ping = '-'
frame_current = '-'
if session:
user = session.online_users.get(item.username)
if user:
ping = str(user['latency'])
metadata = user.get('metadata')
if metadata:
frame_current = str(metadata['frame_current'])
split = layout.split(factor=0.5)
split.label(text=item.username)
split.label(text=frame_current)
split.label(text=ping)
class SESSION_PT_presence(bpy.types.Panel): class SESSION_PT_presence(bpy.types.Panel):
@ -250,7 +263,7 @@ def draw_property(context, parent, property_uuid, level=0):
settings = context.window_manager.session settings = context.window_manager.session
item = operators.client.get(uuid=property_uuid) item = operators.client.get(uuid=property_uuid)
if item.str_type == 'BlUser' or item.state == ERROR: if item.state == ERROR:
return return
area_msg = parent.row(align=True) area_msg = parent.row(align=True)
@ -365,6 +378,7 @@ class SESSION_PT_outliner(bpy.types.Panel):
classes = ( classes = (
SESSION_UL_users,
SESSION_PT_settings, SESSION_PT_settings,
SESSION_PT_settings_user, SESSION_PT_settings_user,
SESSION_PT_settings_network, SESSION_PT_settings_network,

View File

@ -5,6 +5,7 @@ import random
import string import string
import sys import sys
from uuid import uuid4 from uuid import uuid4
from collections.abc import Iterable
import bpy import bpy
import mathutils import mathutils
@ -13,7 +14,18 @@ from . import environment, presence
from .libs import dump_anything from .libs import dump_anything
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR) logger.setLevel(logging.INFO)
def has_action(target):
return (hasattr(target, 'animation_data')
and target.animation_data
and target.animation_data.action)
def has_driver(target):
return (hasattr(target, 'animation_data')
and target.animation_data
and target.animation_data.drivers)
def find_from_attr(attr_name, attr_value, list): def find_from_attr(attr_name, attr_value, list):
@ -45,7 +57,7 @@ def get_datablock_users(datablock):
def random_string_digits(stringLength=6): def random_string_digits(stringLength=6):
"""Generate a random string of letters and digits """ """Generate a random string of letters and digits """
lettersAndDigits = string.ascii_letters + string.digits lettersAndDigits = string.ascii_letters + string.digits
return ''.join(random.choice(lettersAndDigits) for i in range(stringLength)) return ''.join(random.choices(lettersAndDigits, k=stringLength))
def clean_scene(): def clean_scene():
@ -139,3 +151,13 @@ def dump_datablock_attibutes(datablock=None, attributes=[], depth=1, dickt=None)
pass pass
return data return data
def resolve_from_id(id, optionnal_type=None):
for category in dir(bpy.data):
root = getattr(bpy.data, category)
if isinstance(root, Iterable):
if id in root and ((optionnal_type is None) or (optionnal_type.lower() in root[id].__class__.__name__.lower())):
return root[id]
return None