Merge branch '113-support-datablock-conversion' into 'develop'

Resolve "Support datablock conversion"

See merge request slumber/multi-user!44
This commit is contained in:
Swann Martinez
2020-09-18 13:33:43 +00:00
24 changed files with 216 additions and 93 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ from . import environment, utils
DEPENDENCIES = {
("replication", '0.0.21a6'),
("replication", '0.0.21a7'),
}
+1
View File
@@ -134,6 +134,7 @@ class BlAction(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'ACTION_TWEAK'
def _construct(self, data):
+1
View File
@@ -31,6 +31,7 @@ class BlArmature(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 0
bl_automatic_push = True
bl_check_common = False
bl_icon = 'ARMATURE_DATA'
def _construct(self, data):
+1
View File
@@ -29,6 +29,7 @@ class BlCamera(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'CAMERA_DATA'
def _construct(self, data):
+2 -1
View File
@@ -79,7 +79,8 @@ class BlCollection(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = True
def _construct(self, data):
if self.is_library:
with bpy.data.libraries.load(filepath=bpy.data.libraries[self.data['library']].filepath, link=True) as (sourceData, targetData):
+1
View File
@@ -52,6 +52,7 @@ class BlCurve(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'CURVE_DATA'
def _construct(self, data):
+15 -1
View File
@@ -18,6 +18,7 @@
import bpy
import mathutils
import logging
from .. import utils
from .dump_anything import Loader, Dumper
@@ -95,6 +96,7 @@ class BlDatablock(ReplicatedDatablock):
bl_delay_apply : refresh rate in sec for apply
bl_automatic_push : boolean
bl_icon : type icon (blender icon name)
bl_check_common: enable check even in common rights
"""
def __init__(self, *args, **kwargs):
@@ -111,7 +113,7 @@ class BlDatablock(ReplicatedDatablock):
if instance and hasattr(instance, 'uuid'):
instance.uuid = self.uuid
self.diff_method = DIFF_BINARY
# self.diff_method = DIFF_BINARY
def resolve(self):
datablock_ref = None
@@ -122,6 +124,8 @@ class BlDatablock(ReplicatedDatablock):
try:
datablock_ref = datablock_root[self.data['name']]
except Exception:
name = self.data.get('name')
logging.debug(f"Constructing {name}")
datablock_ref = self._construct(data=self.data)
if datablock_ref:
@@ -129,6 +133,15 @@ class BlDatablock(ReplicatedDatablock):
self.instance = datablock_ref
def remove_instance(self):
"""
Remove instance from blender data
"""
assert(self.instance)
datablock_root = getattr(bpy.data, self.bl_id)
datablock_root.remove(self.instance)
def _dump(self, instance=None):
dumper = Dumper()
data = {}
@@ -189,6 +202,7 @@ class BlDatablock(ReplicatedDatablock):
if not self.is_library:
dependencies.extend(self._resolve_deps_implementation())
logging.debug(f"{self.instance.name} dependencies: {dependencies}")
return dependencies
def _resolve_deps_implementation(self):
+1
View File
@@ -218,6 +218,7 @@ class BlGpencil(BlDatablock):
bl_delay_refresh = 2
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'GREASEPENCIL'
def _construct(self, data):
+53 -25
View File
@@ -24,20 +24,39 @@ from .. import utils
from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock
format_to_ext = {
'BMP': 'bmp',
'IRIS': 'sgi',
'PNG': 'png',
'JPEG': 'jpg',
'JPEG2000': 'jp2',
'TARGA': 'tga',
'TARGA_RAW': 'tga',
'CINEON': 'cin',
'DPX': 'dpx',
'OPEN_EXR_MULTILAYER': 'exr',
'OPEN_EXR': 'exr',
'HDR': 'hdr',
'TIFF': 'tiff',
'AVI_JPEG': 'avi',
'AVI_RAW': 'avi',
'FFMPEG': 'mpeg',
}
def dump_image(image):
pixels = None
if image.source == "GENERATED" or image.packed_file is not None:
prefs = utils.get_preferences()
img_name = f"{image.name}.png"
img_name = f"{image.uuid}.{format_to_ext[image.file_format]}"
# Cache the image on the disk
image.filepath_raw = os.path.join(prefs.cache_directory, img_name)
os.makedirs(prefs.cache_directory, exist_ok=True)
image.file_format = "PNG"
image.save()
if image.source == "FILE":
image_path = bpy.path.abspath(image.filepath_raw)
image_path = bpy.path.abspath(image.filepath_raw)
image_directory = os.path.dirname(image_path)
os.makedirs(image_directory, exist_ok=True)
image.save()
@@ -48,28 +67,30 @@ def dump_image(image):
raise ValueError()
return pixels
class BlImage(BlDatablock):
bl_id = "images"
bl_class = bpy.types.Image
bl_delay_refresh = 0
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = False
bl_automatic_push = True
bl_check_common = False
bl_icon = 'IMAGE_DATA'
def _construct(self, data):
return bpy.data.images.new(
name=data['name'],
width=data['size'][0],
height=data['size'][1]
)
name=data['name'],
width=data['size'][0],
height=data['size'][1]
)
def _load(self, data, target):
image = target
prefs = utils.get_preferences()
img_format = data['file_format']
img_name = f"{self.uuid}.{format_to_ext[img_format]}"
img_name = f"{image.name}.png"
img_path = os.path.join(prefs.cache_directory,img_name)
img_path = os.path.join(prefs.cache_directory, img_name)
os.makedirs(prefs.cache_directory, exist_ok=True)
file = open(img_path, 'wb')
file.write(data["pixels"])
@@ -78,7 +99,11 @@ class BlImage(BlDatablock):
image.source = 'FILE'
image.filepath = img_path
image.colorspace_settings.name = data["colorspace_settings"]["name"]
# Unload image from memory
del data['pixels']
loader = Loader()
loader.load(data, target)
def _dump(self, instance=None):
assert(instance)
@@ -86,20 +111,23 @@ class BlImage(BlDatablock):
data['pixels'] = dump_image(instance)
dumper = Dumper()
dumper.depth = 2
dumper.include_filter = [
"name",
'size',
'height',
'alpha',
'float_buffer',
'filepath',
'source',
'colorspace_settings']
dumper.include_filter = [
"name",
'size',
'height',
'alpha',
'float_buffer',
'file_format',
'alpha_mode',
'filepath',
'source',
'colorspace_settings']
data.update(dumper.dump(instance))
return data
def diff(self):
return False
if self.instance and (self.instance.name != self.data['name']):
return True
else:
return False
+1
View File
@@ -32,6 +32,7 @@ class BlLattice(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'LATTICE_DATA'
def _construct(self, data):
+1
View File
@@ -29,6 +29,7 @@ class BlLibrary(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'LIBRARY_DATA_DIRECT'
def _construct(self, data):
+1
View File
@@ -29,6 +29,7 @@ class BlLight(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'LIGHT_DATA'
def _construct(self, data):
+1
View File
@@ -30,6 +30,7 @@ class BlLightprobe(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'LIGHTPROBE_GRID'
def _construct(self, data):
+11 -4
View File
@@ -21,7 +21,7 @@ import mathutils
import logging
import re
from .. import utils
from ..utils import get_datablock_from_uuid
from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock
@@ -39,6 +39,10 @@ def load_node(node_data, node_tree):
target_node = node_tree.nodes.new(type=node_data["bl_idname"])
loader.load(target_node, node_data)
image_uuid = node_data.get('image_uuid', None)
if image_uuid and not target_node.image:
target_node.image = get_datablock_from_uuid(image_uuid,None)
for input in node_data["inputs"]:
if hasattr(target_node.inputs[input], "default_value"):
@@ -118,7 +122,8 @@ def dump_node(node):
"show_preview",
"show_texture",
"outputs",
"width_hidden"
"width_hidden",
"image"
]
dumped_node = node_dumper.dump(node)
@@ -153,7 +158,8 @@ def dump_node(node):
'location'
]
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
if hasattr(node, 'image') and getattr(node, 'image'):
dumped_node['image_uuid'] = node.image.uuid
return dumped_node
@@ -163,6 +169,7 @@ class BlMaterial(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'MATERIAL_DATA'
def _construct(self, data):
@@ -260,7 +267,7 @@ class BlMaterial(BlDatablock):
if self.instance.use_nodes:
for node in self.instance.node_tree.nodes:
if node.type == 'TEX_IMAGE':
if node.type in ['TEX_IMAGE','TEX_ENVIRONMENT']:
deps.append(node.image)
if self.is_library:
deps.append(self.instance.library)
+1
View File
@@ -52,6 +52,7 @@ class BlMesh(BlDatablock):
bl_delay_refresh = 2
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'MESH_DATA'
def _construct(self, data):
+1
View File
@@ -68,6 +68,7 @@ class BlMetaball(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'META_BALL'
def _construct(self, data):
+70 -41
View File
@@ -22,8 +22,10 @@ import bpy
import mathutils
from replication.exception import ContextError
from ..utils import get_datablock_from_uuid
from .bl_datablock import BlDatablock
from .dump_anything import Dumper, Loader
from replication.exception import ReparentException
def load_pose(target_bone, data):
@@ -32,6 +34,45 @@ def load_pose(target_bone, data):
loader.load(target_bone, data)
def find_data_from_name(name=None):
instance = None
if not name:
pass
elif name in bpy.data.meshes.keys():
instance = bpy.data.meshes[name]
elif name in bpy.data.lights.keys():
instance = bpy.data.lights[name]
elif name in bpy.data.cameras.keys():
instance = bpy.data.cameras[name]
elif name in bpy.data.curves.keys():
instance = bpy.data.curves[name]
elif name in bpy.data.metaballs.keys():
instance = bpy.data.metaballs[name]
elif name in bpy.data.armatures.keys():
instance = bpy.data.armatures[name]
elif name in bpy.data.grease_pencils.keys():
instance = bpy.data.grease_pencils[name]
elif name in bpy.data.curves.keys():
instance = bpy.data.curves[name]
elif name in bpy.data.lattices.keys():
instance = bpy.data.lattices[name]
elif name in bpy.data.speakers.keys():
instance = bpy.data.speakers[name]
elif name in bpy.data.lightprobes.keys():
# Only supported since 2.83
if bpy.app.version[1] >= 83:
instance = bpy.data.lightprobes[name]
else:
logging.warning(
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
return instance
def load_data(object, name):
logging.info("loading data")
pass
def _is_editmode(object: bpy.types.Object) -> bool:
child_data = getattr(object, 'data', None)
return (child_data and
@@ -45,6 +86,7 @@ class BlObject(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'OBJECT_DATA'
def _construct(self, data):
@@ -60,36 +102,15 @@ class BlObject(BlDatablock):
return instance
# TODO: refactoring
if "data" not in data:
pass
elif data["data"] in bpy.data.meshes.keys():
instance = bpy.data.meshes[data["data"]]
elif data["data"] in bpy.data.lights.keys():
instance = bpy.data.lights[data["data"]]
elif data["data"] in bpy.data.cameras.keys():
instance = bpy.data.cameras[data["data"]]
elif data["data"] in bpy.data.curves.keys():
instance = bpy.data.curves[data["data"]]
elif data["data"] in bpy.data.metaballs.keys():
instance = bpy.data.metaballs[data["data"]]
elif data["data"] in bpy.data.armatures.keys():
instance = bpy.data.armatures[data["data"]]
elif data["data"] in bpy.data.grease_pencils.keys():
instance = bpy.data.grease_pencils[data["data"]]
elif data["data"] in bpy.data.curves.keys():
instance = bpy.data.curves[data["data"]]
elif data["data"] in bpy.data.lattices.keys():
instance = bpy.data.lattices[data["data"]]
elif data["data"] in bpy.data.speakers.keys():
instance = bpy.data.speakers[data["data"]]
elif data["data"] in bpy.data.lightprobes.keys():
# Only supported since 2.83
if bpy.app.version[1] >= 83:
instance = bpy.data.lightprobes[data["data"]]
else:
logging.warning(
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
instance = bpy.data.objects.new(data["name"], instance)
object_name = data.get("name")
data_uuid = data.get("data_uuid")
data_id = data.get("data")
object_data = get_datablock_from_uuid(
data_uuid,
find_data_from_name(data_id),
ignore=['images']) #TODO: use resolve_from_id
instance = bpy.data.objects.new(object_name, object_data)
instance.uuid = self.uuid
return instance
@@ -97,12 +118,20 @@ class BlObject(BlDatablock):
def _load_implementation(self, data, target):
loader = Loader()
data_uuid = data.get("data_uuid")
data_id = data.get("data")
if target.type != data['type']:
raise ReparentException()
elif target.data and (target.data.name != data_id):
target.data = get_datablock_from_uuid(data_uuid, find_data_from_name(data_id), ignore=['images'])
# vertex groups
if 'vertex_groups' in data:
target.vertex_groups.clear()
for vg in data['vertex_groups']:
vertex_group = target.vertex_groups.new(name=vg['name'])
point_attr = 'vertices' if 'vertices' in vg else 'points'
vertex_group=target.vertex_groups.new(name = vg['name'])
point_attr='vertices' if 'vertices' in vg else 'points'
for vert in vg[point_attr]:
vertex_group.add(
[vert['index']], vert['weight'], 'REPLACE')
@@ -111,12 +140,12 @@ class BlObject(BlDatablock):
if 'shape_keys' in data:
target.shape_key_clear()
object_data = target.data
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)
key_data=data['shape_keys']['key_blocks'][key_block]
target.shape_key_add(name = key_block)
loader.load(
target.data.shape_keys.key_blocks[key_block], key_data)
@@ -162,10 +191,9 @@ class BlObject(BlDatablock):
# TODO: find another way...
if target.type == 'EMPTY':
img_key = data.get('data')
if target.data is None and img_key:
target.data = bpy.data.images.get(img_key, None)
img_uuid = data.get('data_uuid')
if target.data is None and img_uuid:
target.data = get_datablock_from_uuid(img_uuid, None)#bpy.data.images.get(img_key, None)
def _dump_implementation(self, data, instance=None):
assert(instance)
@@ -182,7 +210,7 @@ class BlObject(BlDatablock):
"name",
"rotation_mode",
"parent",
"data",
"data"
"children",
"library",
"empty_display_type",
@@ -202,11 +230,12 @@ class BlObject(BlDatablock):
'lock_location',
'lock_rotation',
'lock_scale',
'type',
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
]
data = dumper.dump(instance)
data['data_uuid'] = getattr(instance.data, 'uuid', None)
if self.is_library:
return data
+1
View File
@@ -30,6 +30,7 @@ class BlScene(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = True
bl_icon = 'SCENE_DATA'
def _construct(self, data):
+1
View File
@@ -29,6 +29,7 @@ class BlSpeaker(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'SPEAKER'
def _load_implementation(self, data, target):
+2 -1
View File
@@ -30,6 +30,7 @@ class BlWorld(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = True
bl_icon = 'WORLD_DATA'
def _construct(self, data):
@@ -84,7 +85,7 @@ class BlWorld(BlDatablock):
if self.instance.use_nodes:
for node in self.instance.node_tree.nodes:
if node.type == 'TEX_IMAGE':
if node.type in ['TEX_IMAGE','TEX_ENVIRONMENT']:
deps.append(node.image)
if self.is_library:
deps.append(self.instance.library)
+34 -17
View File
@@ -21,13 +21,15 @@ import bpy
from . import operators, presence, utils
from replication.constants import (FETCHED,
RP_COMMON,
STATE_INITIAL,
STATE_QUITTING,
STATE_ACTIVE,
STATE_SYNCING,
STATE_LOBBY,
STATE_SRV_SYNC)
UP,
RP_COMMON,
STATE_INITIAL,
STATE_QUITTING,
STATE_ACTIVE,
STATE_SYNCING,
STATE_LOBBY,
STATE_SRV_SYNC,
REPARENT)
class Delayable():
@@ -85,8 +87,8 @@ class ApplyTimer(Timer):
super().__init__(timout)
def execute(self):
client = operators.client
if client and client.state['STATE'] == STATE_ACTIVE:
client = operators.client
if client and client.state['STATE'] == STATE_ACTIVE:
if self._type:
nodes = client.list(filter=self._type)
else:
@@ -100,6 +102,16 @@ class ApplyTimer(Timer):
client.apply(node, force=True)
except Exception as e:
logging.error(f"Fail to apply {node_ref.uuid}: {e}")
elif node_ref.state == REPARENT:
# Reload the node
node_ref.remove_instance()
node_ref.resolve()
client.apply(node, force=True)
for parent in client._graph.find_parents(node):
logging.info(f"Applying parent {parent}")
client.apply(parent, force=True)
node_ref.state = UP
class DynamicRightSelectTimer(Timer):
def __init__(self, timout=.1):
@@ -253,14 +265,16 @@ class ClientUpdate(Timer):
if session and renderer:
if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]:
local_user = operators.client.online_users.get(settings.username)
local_user = operators.client.online_users.get(
settings.username)
if not local_user:
return
else:
for username, user_data in operators.client.online_users.items():
if username != settings.username:
cached_user_data = self.users_metadata.get(username)
cached_user_data = self.users_metadata.get(
username)
new_user_data = operators.client.online_users[username]['metadata']
if cached_user_data is None:
@@ -274,7 +288,7 @@ class ClientUpdate(Timer):
local_user_metadata = local_user.get('metadata')
scene_current = bpy.context.scene.name
local_user = session.online_users.get(settings.username)
local_user = session.online_users.get(settings.username)
current_view_corners = presence.get_view_corners()
# Init client metadata
@@ -283,9 +297,9 @@ class ClientUpdate(Timer):
'view_corners': presence.get_view_matrix(),
'view_matrix': presence.get_view_matrix(),
'color': (settings.client_color.r,
settings.client_color.g,
settings.client_color.b,
1),
settings.client_color.g,
settings.client_color.b,
1),
'frame_current': bpy.context.scene.frame_current,
'scene_current': scene_current
}
@@ -298,9 +312,11 @@ class ClientUpdate(Timer):
session.update_user_metadata(local_user_metadata)
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']:
local_user_metadata['view_corners'] = current_view_corners
local_user_metadata['view_matrix'] = presence.get_view_matrix()
local_user_metadata['view_matrix'] = presence.get_view_matrix(
)
session.update_user_metadata(local_user_metadata)
class SessionStatusUpdate(Timer):
def __init__(self, timout=1):
super().__init__(timout)
@@ -308,6 +324,7 @@ class SessionStatusUpdate(Timer):
def execute(self):
presence.refresh_sidebar_view()
class SessionUserSync(Timer):
def __init__(self, timout=1):
super().__init__(timout)
@@ -332,4 +349,4 @@ class SessionUserSync(Timer):
if user not in ui_users:
new_key = ui_users.add()
new_key.name = user
new_key.username = user
new_key.username = user
+2 -1
View File
@@ -111,7 +111,8 @@ class SessionStartOperator(bpy.types.Operator):
type_module_class.bl_class,
type_module_class,
timer=type_local_config.bl_delay_refresh*1000,
automatic=type_local_config.auto_push)
automatic=type_local_config.auto_push,
check_common=type_module_class.bl_check_common)
if settings.update_method == 'DEFAULT':
if type_local_config.bl_delay_apply > 0:
+2 -1
View File
@@ -61,7 +61,8 @@ def refresh_sidebar_view():
"""
area, region, rv3d = view3d_find()
area.regions[3].tag_redraw()
if area:
area.regions[3].tag_redraw()
def get_target(region, rv3d, coord):
target = [0, 0, 0]
+11
View File
@@ -78,6 +78,17 @@ def resolve_from_id(id, optionnal_type=None):
return root[id]
return None
def get_datablock_from_uuid(uuid, default, ignore=[]):
if not uuid:
return default
for category in dir(bpy.data):
root = getattr(bpy.data, category)
if isinstance(root, Iterable) and category not in ignore:
for item in root:
if getattr(item, 'uuid', None) == uuid:
return item
return default
def get_preferences():
return bpy.context.preferences.addons[__package__].preferences