Compare commits

..

26 Commits

Author SHA1 Message Date
Swann 93d50ac56b refactor: porcail api 2021-04-22 10:04:38 +02:00
Swann 0e73af4d49 refactor: update procelain import 2021-04-21 21:30:43 +02:00
Swann 00d5e622af feat: update replication 2021-04-21 14:33:18 +02:00
Swann b358d3b305 refactor: change push api 2021-04-21 14:32:56 +02:00
Swann 203c10ccd3 feat: missing class attribute for registration 2021-04-21 11:22:53 +02:00
Swann da349dd4a9 refactor: io_bpy architecture revamp 2021-04-21 11:10:24 +02:00
Swann 845bb11b8e fix: bl_object 2021-04-20 09:53:59 +02:00
Swann 9312d6a8c5 Merge branch 'develop' into 173-differential-revision-milestone-2-replication-refactoring 2021-04-20 09:47:23 +02:00
Swann ca78a42076 feat: update submodules 2021-04-06 14:23:10 +02:00
Swann ee886e00c8 feat: generate subtree 2021-04-06 14:21:26 +02:00
Swann 9c7043e84c fix: animation data error 2021-03-26 16:14:27 +01:00
Swann e659b7da94 refactor: move implementation to static def 2021-03-26 12:30:15 +01:00
Swann e3af69a9c8 feat: add replication to the submodules 2021-03-25 14:55:53 +01:00
Swann 328c651cea remove data handlign from ReplicatedDatablock 2021-03-24 16:08:12 +01:00
Swann d4224c789a refactor: move commit to porcelain
feractor: remove is_ readonly
2021-03-24 11:20:40 +01:00
Swann 3a02711baa feat: faster root management 2021-03-14 20:58:25 +01:00
Swann c7e8002fed fix: apply api
clean: ipc port propertie
2021-03-14 18:32:04 +01:00
Swann f4e7ec6be8 Merge branch 'develop' into 173-differential-revision-milestone-2-replication-refactoring 2021-03-14 17:46:23 +01:00
Swann 235db712fd fix: api 2021-03-11 15:45:48 +01:00
Swann 647ac46c01 feat: move apply to porcelain
feat: move data access to repository
feat: object_store layer to repository (with GraphObjectStore)
revert: missing network services
2021-03-09 14:07:59 +01:00
Swann 8e3c86561f refactor: move add to porcelain 2021-03-09 10:19:51 +01:00
Swann dba19e831d Merge branch 'develop' into 173-differential-revision-milestone-2-replication-refactoring 2021-03-08 22:16:14 +01:00
Swann 93df5ca5fa fix: disconnect callback 2021-03-06 10:20:57 +01:00
Swann b17104c67e fix: naming 2021-03-05 10:35:35 +01:00
Swann 875b9ce934 feat: temporary disable CI jobs for this branch because of breaking changes 2021-03-04 14:24:03 +01:00
Swann 2d638ef76f refactor: interface api changes 2021-03-04 14:22:54 +01:00
59 changed files with 924 additions and 1485 deletions
-13
View File
@@ -1,13 +0,0 @@
stages:
- test
- build
- deploy
- doc
include:
- local: .gitlab/ci/test.gitlab-ci.yml
- local: .gitlab/ci/build.gitlab-ci.yml
- local: .gitlab/ci/deploy.gitlab-ci.yml
- local: .gitlab/ci/doc.gitlab-ci.yml
+3
View File
@@ -0,0 +1,3 @@
[submodule "multi_user/libs/replication"]
path = multi_user/libs/replication
url = https://gitlab.com/slumber/replication
-9
View File
@@ -374,15 +374,6 @@ Network
Advanced network settings
**IPC Port** is the port used for Inter Process Communication. This port is used
by the multi-user subprocesses to communicate with each other. If different instances
of multi-user are using the same IPC port, this will create conflict !
.. note::
You only need to modify this setting if you need to launch multiple clients from the same
computer (or if you try to host and join from the same computer). To resolve this, you simply need to enter a different
**IPC port** for each blender instance.
**Timeout (in milliseconds)** is the maximum ping authorized before auto-disconnecting.
You should only increase it if you have a bad connection.
+11 -14
View File
@@ -41,12 +41,8 @@ import bpy
from bpy.app.handlers import persistent
from . import environment
DEPENDENCIES = {
("replication", '0.1.26'),
}
from uuid import uuid4
LIBS = os.path.dirname(os.path.abspath(__file__))+"/libs/replication"
module_error_msg = "Insufficient rights to install the multi-user \
dependencies, aunch blender with administrator rights."
@@ -57,14 +53,15 @@ def register():
datefmt='%H:%M:%S',
level=logging.INFO)
for module_name in list(sys.modules.keys()):
if 'replication' in module_name:
del sys.modules[module_name]
if LIBS not in sys.path:
logging.info('Adding local modules dir to the path')
sys.path.insert(0, LIBS)
try:
if bpy.app.version[1] >= 91:
python_binary_path = sys.executable
else:
python_binary_path = bpy.app.binary_path_python
environment.setup(DEPENDENCIES, python_binary_path)
from . import presence
from . import operators
from . import ui
@@ -78,7 +75,7 @@ def register():
ui.register()
except ModuleNotFoundError as e:
raise Exception(module_error_msg)
logging.error(module_error_msg)
logging.error(e)
bpy.types.WindowManager.session = bpy.props.PointerProperty(
type=preferences.SessionProps)
-56
View File
@@ -1,56 +0,0 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# ##### END GPL LICENSE BLOCK #####
import bpy
__all__ = [
'bl_object',
'bl_mesh',
'bl_camera',
'bl_collection',
'bl_curve',
'bl_gpencil',
'bl_image',
'bl_light',
'bl_scene',
'bl_material',
'bl_library',
'bl_armature',
'bl_action',
'bl_world',
'bl_metaball',
'bl_lattice',
'bl_lightprobe',
'bl_speaker',
'bl_font',
'bl_sound',
'bl_file',
# 'bl_sequencer',
'bl_node_group',
'bl_texture',
"bl_particle",
] # Order here defines execution order
if bpy.app.version[1] >= 91:
__all__.append('bl_volume')
from . import *
from replication.data import ReplicatedDataFactory
def types_to_register():
return __all__
-231
View File
@@ -1,231 +0,0 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# ##### END GPL LICENSE BLOCK #####
import logging
from collections.abc import Iterable
import bpy
import mathutils
from replication.constants import DIFF_BINARY, DIFF_JSON, UP
from replication.data import ReplicatedDatablock
from .. import utils
from .dump_anything import Dumper, Loader
def has_action(target):
""" Check if the target datablock has actions
"""
return (hasattr(target, 'animation_data')
and target.animation_data
and target.animation_data.action)
def has_driver(target):
""" Check if the target datablock is driven
"""
return (hasattr(target, 'animation_data')
and target.animation_data
and target.animation_data.drivers)
def dump_driver(driver):
dumper = Dumper()
dumper.depth = 6
data = dumper.dump(driver)
return data
def load_driver(target_datablock, src_driver):
loader = Loader()
drivers = target_datablock.animation_data.drivers
src_driver_data = src_driver['driver']
new_driver = drivers.new(src_driver['data_path'], index=src_driver['array_index'])
# Settings
new_driver.driver.type = src_driver_data['type']
new_driver.driver.expression = src_driver_data['expression']
loader.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'])
loader.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]
loader.load(new_point, src_driver['keyframe_points'][src_point])
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
class BlDatablock(ReplicatedDatablock):
"""BlDatablock
bl_id : blender internal storage identifier
bl_class : blender internal type
bl_icon : type icon (blender icon name)
bl_check_common: enable check even in common rights
bl_reload_parent: reload parent
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
self.preferences = utils.get_preferences()
# TODO: use is_library_indirect
self.is_library = (instance and hasattr(instance, 'library') and
instance.library) or \
(hasattr(self,'data') and self.data and 'library' in self.data)
if instance and hasattr(instance, 'uuid'):
instance.uuid = self.uuid
def resolve(self, construct = True):
datablock_root = getattr(bpy.data, self.bl_id)
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
if not datablock_ref:
try:
datablock_ref = datablock_root[self.data['name']]
except Exception:
pass
if construct and not datablock_ref:
name = self.data.get('name')
logging.debug(f"Constructing {name}")
datablock_ref = self._construct(data=self.data)
if datablock_ref is not None:
setattr(datablock_ref, 'uuid', self.uuid)
self.instance = datablock_ref
return True
else:
return False
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 = {}
# Dump animation data
if has_action(instance):
dumper = Dumper()
dumper.include_filter = ['action']
data['animation_data'] = dumper.dump(instance.animation_data)
if has_driver(instance):
dumped_drivers = {'animation_data': {'drivers': []}}
for driver in instance.animation_data.drivers:
dumped_drivers['animation_data']['drivers'].append(
dump_driver(driver))
data.update(dumped_drivers)
if self.is_library:
data.update(dumper.dump(instance))
else:
data.update(self._dump_implementation(data, instance=instance))
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']]
# Remove existing animation data if there is not more to load
elif hasattr(target, 'animation_data') and target.animation_data:
target.animation_data_clear()
if self.is_library:
return
else:
self._load_implementation(data, target)
def _load_implementation(self, data, target):
raise NotImplementedError
def resolve_deps(self):
dependencies = []
if has_action(self.instance):
dependencies.append(self.instance.animation_data.action)
if not self.is_library:
dependencies.extend(self._resolve_deps_implementation())
logging.debug(f"{self.instance} dependencies: {dependencies}")
return dependencies
def _resolve_deps_implementation(self):
return []
def is_valid(self):
return getattr(bpy.data, self.bl_id).get(self.data['name'])
+62
View File
@@ -0,0 +1,62 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# ##### END GPL LICENSE BLOCK #####
import bpy
from replication.protocol import ReplicatedDatablock
__all__ = [
'bl_object',
'bl_mesh',
# 'bl_camera',
'bl_collection',
# 'bl_curve',
# 'bl_gpencil',
# 'bl_image',
# 'bl_light',
'bl_scene',
'bl_material',
# 'bl_library',
# 'bl_armature',
# 'bl_action',
# 'bl_world',
# 'bl_metaball',
# 'bl_lattice',
# 'bl_lightprobe',
# 'bl_speaker',
# 'bl_font',
# 'bl_sound',
# 'bl_file',
# 'bl_sequencer',
# 'bl_node_group',
# 'bl_texture',
# "bl_particle",
] # Order here defines execution order
# if bpy.app.version[1] >= 91:
# __all__.append('bl_volume')
from replication.protocol import DataTranslationProtocol
def get_data_translation_protocol()-> DataTranslationProtocol:
""" Return a data translation protocol from implemented bpy types
"""
bpy_protocol = DataTranslationProtocol()
for module_name in __all__:
impl = globals().get(module_name)
if impl and hasattr(impl, "_type") and hasattr(impl, "_type"):
bpy_protocol.register_implementation(impl._type, impl._class)
return bpy_protocol
@@ -24,9 +24,14 @@ from enum import Enum
from .. import utils
from .dump_anything import (
Dumper, Loader, np_dump_collection, np_load_collection, remove_items_from_dict)
from .bl_datablock import BlDatablock
Dumper,
Loader,
np_dump_collection,
np_load_collection,
remove_items_from_dict)
from .bl_datablock import stamp_uuid
from replication.protocol import ReplicatedDatablock
from replication.objects import Node
KEYFRAME = [
'amplitude',
@@ -42,6 +47,68 @@ KEYFRAME = [
]
def has_action(datablock):
""" Check if the datablock datablock has actions
"""
return (hasattr(datablock, 'animation_data')
and datablock.animation_data
and datablock.animation_data.action)
def has_driver(datablock):
""" Check if the datablock datablock is driven
"""
return (hasattr(datablock, 'animation_data')
and datablock.animation_data
and datablock.animation_data.drivers)
def dump_driver(driver):
dumper = Dumper()
dumper.depth = 6
data = dumper.dump(driver)
return data
def load_driver(target_datablock, src_driver):
loader = Loader()
drivers = target_datablock.animation_data.drivers
src_driver_data = src_driver['driver']
new_driver = drivers.new(
src_driver['data_path'], index=src_driver['array_index'])
# Settings
new_driver.driver.type = src_driver_data['type']
new_driver.driver.expression = src_driver_data['expression']
loader.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'])
loader.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]
loader.load(new_point, src_driver['keyframe_points'][src_point])
def dump_fcurve(fcurve: bpy.types.FCurve, use_numpy: bool = True) -> dict:
""" Dump a sigle curve to a dict
@@ -129,26 +196,70 @@ def load_fcurve(fcurve_data, fcurve):
fcurve.update()
class BlAction(BlDatablock):
def dump_animation_data(datablock, data):
if has_action(datablock):
dumper = Dumper()
dumper.include_filter = ['action']
data['animation_data'] = dumper.dump(datablock.animation_data)
if has_driver(datablock):
dumped_drivers = {'animation_data': {'drivers': []}}
for driver in datablock.animation_data.drivers:
dumped_drivers['animation_data']['drivers'].append(
dump_driver(driver))
data.update(dumped_drivers)
def load_animation_data(data, datablock):
# Load animation data
if 'animation_data' in data.keys():
if datablock.animation_data is None:
datablock.animation_data_create()
for d in datablock.animation_data.drivers:
datablock.animation_data.drivers.remove(d)
if 'drivers' in data['animation_data']:
for driver in data['animation_data']['drivers']:
load_driver(datablock, driver)
if 'action' in data['animation_data']:
datablock.animation_data.action = bpy.data.actions[data['animation_data']['action']]
# Remove existing animation data if there is not more to load
elif hasattr(datablock, 'animation_data') and datablock.animation_data:
datablock.animation_data_clear()
def resolve_animation_dependencies(datablock):
if has_action(datablock):
return [datablock.animation_data.action]
else:
return []
class BlAction(ReplicatedDatablock):
bl_id = "actions"
bl_class = bpy.types.Action
bl_check_common = False
bl_icon = 'ACTION_TWEAK'
bl_reload_parent = False
def _construct(self, data):
@staticmethod
def construct(data: dict) -> object:
return bpy.data.actions.new(data["name"])
def _load_implementation(self, data, target):
@staticmethod
def load(data: dict, datablock: object):
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(
fcurve = datablock.fcurves.find(
dumped_data_path, index=dumped_array_index)
if fcurve is None:
fcurve = target.fcurves.new(
fcurve = datablock.fcurves.new(
dumped_data_path, index=dumped_array_index)
load_fcurve(dumped_fcurve, fcurve)
@@ -156,9 +267,12 @@ class BlAction(BlDatablock):
id_root = data.get('id_root')
if id_root:
target.id_root = id_root
datablock.id_root = id_root
@staticmethod
def dump(datablock: object) -> dict:
stamp_uuid(datablock)
def _dump_implementation(self, data, instance=None):
dumper = Dumper()
dumper.exclude_filter = [
'name_full',
@@ -173,11 +287,11 @@ class BlAction(BlDatablock):
'users'
]
dumper.depth = 1
data = dumper.dump(instance)
data = dumper.dump(datablock)
data["fcurves"] = []
for fcurve in instance.fcurves:
for fcurve in datablock.fcurves:
data["fcurves"].append(dump_fcurve(fcurve, use_numpy=True))
return data
@@ -42,10 +42,10 @@ class BlArmature(BlDatablock):
bl_icon = 'ARMATURE_DATA'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.armatures.new(data["name"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
# Load parent object
parent_object = utils.find_from_attr(
'uuid',
@@ -119,7 +119,7 @@ class BlArmature(BlDatablock):
if 'EDIT' in current_mode:
bpy.ops.object.mode_set(mode='EDIT')
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
dumper = Dumper()
@@ -30,11 +30,11 @@ class BlCamera(BlDatablock):
bl_icon = 'CAMERA_DATA'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.cameras.new(data["name"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
@@ -56,7 +56,7 @@ class BlCamera(BlDatablock):
target_img.image = bpy.data.images[img_id]
loader.load(target_img, img_data)
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
# TODO: background image support
@@ -105,9 +105,10 @@ class BlCamera(BlDatablock):
]
return dumper.dump(instance)
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
for background in self.instance.background_images:
for background in datablock.background_images:
if background.image:
deps.append(background.image)
@@ -20,9 +20,9 @@ import bpy
import mathutils
from .. import utils
from .bl_datablock import BlDatablock
from .dump_anything import Loader, Dumper
from replication.protocol import ReplicatedDatablock
from replication.objects import Node
def dump_collection_children(collection):
collection_children = []
@@ -81,42 +81,37 @@ def resolve_collection_dependencies(collection):
return deps
class BlCollection(BlDatablock):
class BlCollection(ReplicatedDatablock):
bl_id = "collections"
bl_icon = 'FILE_FOLDER'
bl_class = bpy.types.Collection
bl_check_common = True
bl_reload_parent = False
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):
targetData.collections = [
name for name in sourceData.collections if name == self.data['name']]
@staticmethod
def construct(data: dict) -> object:
datablock = bpy.data.collections.new(node.data["name"])
return datablock
instance = bpy.data.collections[self.data['name']]
return instance
instance = bpy.data.collections.new(data["name"])
return instance
def _load_implementation(self, data, target):
@staticmethod
def load(data: dict, datablock: object):
data = node.data
loader = Loader()
loader.load(target, data)
loader.load(datablock, data)
# Objects
load_collection_objects(data['objects'], target)
load_collection_objects(data['objects'], datablock)
# Link childrens
load_collection_childrens(data['children'], target)
load_collection_childrens(data['children'], datablock)
# FIXME: Find a better way after the replication big refacotoring
# Keep other user from deleting collection object by flushing their history
utils.flush_history()
def _dump_implementation(self, data, instance=None):
assert(instance)
@staticmethod
def dump(datablock: object) -> dict:
assert(datablock)
dumper = Dumper()
dumper.depth = 1
@@ -124,15 +119,19 @@ class BlCollection(BlDatablock):
"name",
"instance_offset"
]
data = dumper.dump(instance)
data = dumper.dump(datablock)
# dump objects
data['objects'] = dump_collection_objects(instance)
data['objects'] = dump_collection_objects(datablock)
# dump children collections
data['children'] = dump_collection_children(instance)
data['children'] = dump_collection_children(datablock)
return data
def _resolve_deps_implementation(self):
return resolve_collection_dependencies(self.instance)
@staticmethod
def resolve_deps(datablock: object) -> [object]:
return resolve_collection_dependencies(datablock)
_type = bpy.types.Collection
_class = BlCollection
@@ -141,10 +141,10 @@ class BlCurve(BlDatablock):
bl_icon = 'CURVE_DATA'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.curves.new(data["name"], data["type"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
@@ -175,7 +175,7 @@ class BlCurve(BlDatablock):
if src_materials:
load_materials_slots(src_materials, target.materials)
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
dumper = Dumper()
# Conflicting attributes
@@ -222,10 +222,10 @@ class BlCurve(BlDatablock):
return data
def _resolve_deps_implementation(self):
# TODO: resolve material
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
curve = self.instance
curve = datablock
if isinstance(curve, T.TextCurve):
deps.extend([
@@ -234,7 +234,7 @@ class BlCurve(BlDatablock):
curve.font_bold_italic,
curve.font_italic])
for material in self.instance.materials:
for material in curve.materials:
if material:
deps.append(material)
+59
View File
@@ -0,0 +1,59 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# ##### END GPL LICENSE BLOCK #####
import logging
from collections.abc import Iterable
import bpy
import mathutils
from replication.constants import DIFF_BINARY, DIFF_JSON, UP
from replication.protocol import ReplicatedDatablock
from replication.objects import Node
from uuid import uuid4
from .. import utils
from .dump_anything import Dumper, Loader
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 resolve_datablock_from_root(node:Node, root)->object:
datablock_ref = utils.find_from_attr('uuid', node.uuid, root)
if not datablock_ref:
try:
datablock_ref = root[node.data['name']]
except Exception:
pass
return datablock_ref
def stamp_uuid(datablock):
if not datablock.uuid:
datablock.uuid = str(uuid4())
@@ -24,7 +24,7 @@ from pathlib import Path
import bpy
import mathutils
from replication.constants import DIFF_BINARY, UP
from replication.data import ReplicatedDatablock
from replication.protocol import ReplicatedDatablock
from .. import utils
from .dump_anything import Dumper, Loader
@@ -84,7 +84,7 @@ class BlFile(ReplicatedDatablock):
if self.preferences.clear_memory_filecache:
del self.data['file']
def _dump(self, instance=None):
def dump(self, instance=None):
"""
Read the file and return a dict as:
{
@@ -114,7 +114,7 @@ class BlFile(ReplicatedDatablock):
return data
def _load(self, data, target):
def load(self, data, target):
"""
Writing the file
"""
@@ -34,7 +34,7 @@ class BlFont(BlDatablock):
bl_icon = 'FILE_FONT'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
filename = data.get('filename')
if filename == '<builtin>':
@@ -42,10 +42,10 @@ class BlFont(BlDatablock):
else:
return bpy.data.fonts.load(get_filepath(filename))
def _load(self, data, target):
def load(self, data, target):
pass
def _dump(self, instance=None):
def dump(self, instance=None):
if instance.filepath == '<builtin>':
filename = '<builtin>'
else:
@@ -62,11 +62,12 @@ class BlFont(BlDatablock):
def diff(self):
return False
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
if self.instance.filepath and self.instance.filepath != '<builtin>':
ensure_unpacked(self.instance)
if datablock.filepath and datablock.filepath != '<builtin>':
ensure_unpacked(datablock)
deps.append(Path(bpy.path.abspath(self.instance.filepath)))
deps.append(Path(bpy.path.abspath(datablock.filepath)))
return deps
@@ -235,10 +235,10 @@ class BlGpencil(BlDatablock):
bl_icon = 'GREASEPENCIL'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.grease_pencils.new(data["name"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
target.materials.clear()
if "materials" in data.keys():
for mat in data['materials']:
@@ -267,7 +267,7 @@ class BlGpencil(BlDatablock):
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
dumper = Dumper()
dumper.depth = 2
@@ -290,10 +290,11 @@ class BlGpencil(BlDatablock):
data["eval_frame"] = bpy.context.scene.frame_current
return data
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
for material in self.instance.materials:
for material in datablock.materials:
deps.append(material)
return deps
@@ -55,14 +55,14 @@ class BlImage(BlDatablock):
bl_icon = 'IMAGE_DATA'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.images.new(
name=data['name'],
width=data['size'][0],
height=data['size'][1]
)
def _load(self, data, target):
def load(self, data, target):
loader = Loader()
loader.load(data, target)
@@ -70,7 +70,7 @@ class BlImage(BlDatablock):
target.filepath_raw = get_filepath(data['filename'])
target.colorspace_settings.name = data["colorspace_settings"]["name"]
def _dump(self, instance=None):
def dump(self, instance=None):
assert(instance)
filename = Path(instance.filepath).name
@@ -101,23 +101,24 @@ class BlImage(BlDatablock):
else:
return False
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
if self.instance.packed_file:
filename = Path(bpy.path.abspath(self.instance.filepath)).name
self.instance.filepath_raw = get_filepath(filename)
self.instance.save()
if datablock.packed_file:
filename = Path(bpy.path.abspath(datablock.filepath)).name
datablock.filepath_raw = get_filepath(filename)
datablock.save()
# An image can't be unpacked to the modified path
# TODO: make a bug report
self.instance.unpack(method="REMOVE")
datablock.unpack(method="REMOVE")
elif self.instance.source == "GENERATED":
filename = f"{self.instance.name}.png"
self.instance.filepath = get_filepath(filename)
self.instance.save()
elif datablock.source == "GENERATED":
filename = f"{datablock.name}.png"
datablock.filepath = get_filepath(filename)
datablock.save()
if self.instance.filepath:
deps.append(Path(bpy.path.abspath(self.instance.filepath)))
if datablock.filepath:
deps.append(Path(bpy.path.abspath(datablock.filepath)))
return deps
@@ -33,10 +33,10 @@ class BlLattice(BlDatablock):
bl_icon = 'LATTICE_DATA'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.lattices.new(data["name"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
if target.is_editmode:
raise ContextError("lattice is in edit mode")
@@ -45,7 +45,7 @@ class BlLattice(BlDatablock):
np_load_collection(data['points'], target.points, POINT)
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
if instance.is_editmode:
raise ContextError("lattice is in edit mode")
@@ -30,14 +30,14 @@ class BlLibrary(BlDatablock):
bl_icon = 'LIBRARY_DATA_DIRECT'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
with bpy.data.libraries.load(filepath=data["filepath"], link=True) as (sourceData, targetData):
targetData = sourceData
return sourceData
def _load(self, data, target):
def load(self, data, target):
pass
def _dump(self, instance=None):
def dump(self, instance=None):
assert(instance)
dumper = Dumper()
return dumper.dump(instance)
@@ -30,14 +30,14 @@ class BlLight(BlDatablock):
bl_icon = 'LIGHT_DATA'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.lights.new(data["name"], data["type"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
dumper = Dumper()
dumper.depth = 3
@@ -31,7 +31,7 @@ class BlLightprobe(BlDatablock):
bl_icon = 'LIGHTPROBE_GRID'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type']
# See https://developer.blender.org/D6396
if bpy.app.version[1] >= 83:
@@ -39,11 +39,11 @@ class BlLightprobe(BlDatablock):
else:
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
if bpy.app.version[1] < 83:
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
@@ -24,7 +24,9 @@ import re
from uuid import uuid4
from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock, get_datablock_from_uuid
from .bl_datablock import get_datablock_from_uuid, stamp_uuid
from replication.protocol import ReplicatedDatablock
from replication.objects import Node
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM']
@@ -34,7 +36,7 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
:arg node_data: dumped node data
:type node_data: dict
:arg node_tree: target node_tree
:arg node_tree: datablock node_tree
:type node_tree: bpy.types.NodeTree
"""
loader = Loader()
@@ -88,7 +90,7 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
def dump_node(node: bpy.types.ShaderNode) -> dict:
""" Dump a single node to a dict
:arg node: target node
:arg node: datablock node
:type node: bpy.types.Node
:retrun: dict
"""
@@ -249,7 +251,7 @@ def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
def dump_node_tree_sockets(sockets: bpy.types.Collection) -> dict:
""" dump sockets of a shader_node_tree
:arg target_node_tree: target node_tree
:arg target_node_tree: datablock node_tree
:type target_node_tree: bpy.types.NodeTree
:arg socket_id: socket identifer
:type socket_id: str
@@ -272,7 +274,7 @@ def load_node_tree_sockets(sockets: bpy.types.Collection,
sockets_data: dict):
""" load sockets of a shader_node_tree
:arg target_node_tree: target node_tree
:arg target_node_tree: datablock node_tree
:type target_node_tree: bpy.types.NodeTree
:arg socket_id: socket identifer
:type socket_id: str
@@ -300,7 +302,7 @@ def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeT
:arg node_tree_data: dumped node data
:type node_tree_data: dict
:arg target_node_tree: target node_tree
:arg target_node_tree: datablock node_tree
:type target_node_tree: bpy.types.NodeTree
"""
# TODO: load only required nodes
@@ -373,7 +375,7 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
:arg src_materials: dumped material collection (ex: object.materials)
:type src_materials: list of tuples (uuid, name)
:arg dst_materials: target material collection pointer
:arg dst_materials: datablock material collection pointer
:type dst_materials: bpy.types.bpy_prop_collection
"""
# MATERIAL SLOTS
@@ -389,36 +391,41 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
dst_materials.append(mat_ref)
class BlMaterial(BlDatablock):
class BlMaterial(ReplicatedDatablock):
bl_id = "materials"
bl_class = bpy.types.Material
bl_check_common = False
bl_icon = 'MATERIAL_DATA'
bl_reload_parent = False
def _construct(self, data):
@staticmethod
def construct(data: dict) -> object:
return bpy.data.materials.new(data["name"])
def _load_implementation(self, data, target):
@staticmethod
def load(data: dict, datablock: object):
data = data
loader = Loader()
is_grease_pencil = data.get('is_grease_pencil')
use_nodes = data.get('use_nodes')
loader.load(target, data)
loader.load(datablock, data)
if is_grease_pencil:
if not target.is_grease_pencil:
bpy.data.materials.create_gpencil_data(target)
loader.load(target.grease_pencil, data['grease_pencil'])
if not datablock.is_grease_pencil:
bpy.data.materials.create_gpencil_data(datablock)
loader.load(datablock.grease_pencil, data['grease_pencil'])
elif use_nodes:
if target.node_tree is None:
target.use_nodes = True
if datablock.node_tree is None:
datablock.use_nodes = True
load_node_tree(data['node_tree'], target.node_tree)
load_node_tree(data['node_tree'], datablock.node_tree)
@staticmethod
def dump(datablock: object) -> dict:
stamp_uuid(datablock)
def _dump_implementation(self, data, instance=None):
assert(instance)
mat_dumper = Dumper()
mat_dumper.depth = 2
mat_dumper.include_filter = [
@@ -444,9 +451,9 @@ class BlMaterial(BlDatablock):
'line_priority',
'is_grease_pencil'
]
data = mat_dumper.dump(instance)
data = mat_dumper.dump(datablock)
if instance.is_grease_pencil:
if datablock.is_grease_pencil:
gp_mat_dumper = Dumper()
gp_mat_dumper.depth = 3
@@ -480,19 +487,21 @@ class BlMaterial(BlDatablock):
'use_overlap_strokes',
'use_fill_holdout',
]
data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil)
elif instance.use_nodes:
data['node_tree'] = dump_node_tree(instance.node_tree)
data['grease_pencil'] = gp_mat_dumper.dump(datablock.grease_pencil)
elif datablock.use_nodes:
data['node_tree'] = dump_node_tree(datablock.node_tree)
return data
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
# TODO: resolve node group deps
deps = []
if self.instance.use_nodes:
deps.extend(get_node_tree_dependencies(self.instance.node_tree))
if self.is_library:
deps.append(self.instance.library)
if datablock.use_nodes:
deps.extend(get_node_tree_dependencies(datablock.node_tree))
return deps
_type = bpy.types.Material
_class = BlMaterial
@@ -22,12 +22,21 @@ import mathutils
import logging
import numpy as np
from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection
from .dump_anything import (Dumper,
Loader,
np_load_collection_primitives,
np_dump_collection_primitive,
np_load_collection, np_dump_collection)
from replication.constants import DIFF_BINARY
from replication.exception import ContextError
from .bl_datablock import BlDatablock, get_datablock_from_uuid
from replication.protocol import ReplicatedDatablock
from replication.objects import Node
from .bl_datablock import get_datablock_from_uuid, stamp_uuid
from .bl_material import dump_materials_slots, load_materials_slots
from ..preferences import get_preferences
VERTICE = ['co']
EDGE = [
@@ -49,80 +58,87 @@ POLYGON = [
'material_index',
]
class BlMesh(BlDatablock):
class BlMesh(ReplicatedDatablock):
bl_id = "meshes"
bl_class = bpy.types.Mesh
bl_check_common = False
bl_icon = 'MESH_DATA'
bl_reload_parent = True
def _construct(self, data):
instance = bpy.data.meshes.new(data["name"])
instance.uuid = self.uuid
return instance
@staticmethod
def construct(data: dict) -> object:
datablock = bpy.data.meshes.new(data["name"])
datablock.uuid = data['uuid']
return datablock
def _load_implementation(self, data, target):
if not target or target.is_editmode:
@staticmethod
def load(data: dict, datablock: object):
data = data
if not datablock or datablock.is_editmode:
raise ContextError
else:
loader = Loader()
loader.load(target, data)
loader.load(datablock, data)
# MATERIAL SLOTS
src_materials = data.get('materials', None)
if src_materials:
load_materials_slots(src_materials, target.materials)
load_materials_slots(src_materials, datablock.materials)
# CLEAR GEOMETRY
if target.vertices:
target.clear_geometry()
if datablock.vertices:
datablock.clear_geometry()
target.vertices.add(data["vertex_count"])
target.edges.add(data["egdes_count"])
target.loops.add(data["loop_count"])
target.polygons.add(data["poly_count"])
datablock.vertices.add(data["vertex_count"])
datablock.edges.add(data["egdes_count"])
datablock.loops.add(data["loop_count"])
datablock.polygons.add(data["poly_count"])
# LOADING
np_load_collection(data['vertices'], target.vertices, VERTICE)
np_load_collection(data['edges'], target.edges, EDGE)
np_load_collection(data['loops'], target.loops, LOOP)
np_load_collection(data["polygons"],target.polygons, POLYGON)
np_load_collection(data['vertices'], datablock.vertices, VERTICE)
np_load_collection(data['edges'], datablock.edges, EDGE)
np_load_collection(data['loops'], datablock.loops, LOOP)
np_load_collection(data["polygons"], datablock.polygons, POLYGON)
# UV Layers
if 'uv_layers' in data.keys():
for layer in data['uv_layers']:
if layer not in target.uv_layers:
target.uv_layers.new(name=layer)
if layer not in datablock.uv_layers:
datablock.uv_layers.new(name=layer)
np_load_collection_primitives(
target.uv_layers[layer].data,
'uv',
datablock.uv_layers[layer].data,
'uv',
data["uv_layers"][layer]['data'])
# Vertex color
if 'vertex_colors' in data.keys():
for color_layer in data['vertex_colors']:
if color_layer not in target.vertex_colors:
target.vertex_colors.new(name=color_layer)
if color_layer not in datablock.vertex_colors:
datablock.vertex_colors.new(name=color_layer)
np_load_collection_primitives(
target.vertex_colors[color_layer].data,
'color',
datablock.vertex_colors[color_layer].data,
'color',
data["vertex_colors"][color_layer]['data'])
target.validate()
target.update()
datablock.validate()
datablock.update()
def _dump_implementation(self, data, instance=None):
assert(instance)
@staticmethod
def dump(datablock: object) -> dict:
stamp_uuid(datablock)
if (instance.is_editmode or bpy.context.mode == "SCULPT") and not self.preferences.sync_flags.sync_during_editmode:
if (datablock.is_editmode or bpy.context.mode == "SCULPT") and not get_preferences().sync_flags.sync_during_editmode:
raise ContextError("Mesh is in edit mode")
mesh = instance
mesh = datablock
dumper = Dumper()
dumper.depth = 1
dumper.include_filter = [
'uuid'
'name',
'use_auto_smooth',
'auto_smooth_angle',
@@ -153,23 +169,26 @@ class BlMesh(BlDatablock):
data['uv_layers'] = {}
for layer in mesh.uv_layers:
data['uv_layers'][layer.name] = {}
data['uv_layers'][layer.name]['data'] = np_dump_collection_primitive(layer.data, 'uv')
data['uv_layers'][layer.name]['data'] = np_dump_collection_primitive(
layer.data, 'uv')
# Vertex color
if mesh.vertex_colors:
data['vertex_colors'] = {}
for color_map in mesh.vertex_colors:
data['vertex_colors'][color_map.name] = {}
data['vertex_colors'][color_map.name]['data'] = np_dump_collection_primitive(color_map.data, 'color')
data['vertex_colors'][color_map.name]['data'] = np_dump_collection_primitive(
color_map.data, 'color')
# Materials
data['materials'] = dump_materials_slots(instance.materials)
data['materials'] = dump_materials_slots(datablock.materials)
return data
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
for material in self.instance.materials:
for material in datablock.materials:
if material:
deps.append(material)
@@ -177,7 +196,10 @@ class BlMesh(BlDatablock):
def diff(self):
if 'EDIT' in bpy.context.mode \
and not self.preferences.sync_flags.sync_during_editmode:
and not get_preferences().sync_flags.sync_during_editmode:
return False
else:
return super().diff()
_type = bpy.types.Mesh
_class = BlMesh
@@ -69,10 +69,10 @@ class BlMetaball(BlDatablock):
bl_icon = 'META_BALL'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.metaballs.new(data["name"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
@@ -83,7 +83,7 @@ class BlMetaball(BlDatablock):
load_metaball_elements(data['elements'], target.elements)
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
dumper = Dumper()
dumper.depth = 1
@@ -32,14 +32,15 @@ class BlNodeGroup(BlDatablock):
bl_icon = 'NODETREE'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.node_groups.new(data["name"], data["type"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
load_node_tree(data, target)
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
return dump_node_tree(instance)
def _resolve_deps_implementation(self):
return get_node_tree_dependencies(self.instance)
@staticmethod
def resolve_deps(datablock: object) -> [object]:
return get_node_tree_dependencies(datablock)
@@ -21,8 +21,16 @@ import re
import bpy
import mathutils
from replication.exception import ContextError
from replication.objects import Node
from replication.protocol import ReplicatedDatablock
from .bl_datablock import BlDatablock, get_datablock_from_uuid
from .bl_datablock import get_datablock_from_uuid, stamp_uuid
from .bl_action import (load_animation_data,
dump_animation_data,
resolve_animation_dependencies)
from ..preferences import get_preferences
from .bl_datablock import get_datablock_from_uuid
from .bl_material import IGNORED_SOCKETS
from .dump_anything import (
Dumper,
@@ -159,45 +167,40 @@ def load_pose(target_bone, data):
def find_data_from_name(name=None):
instance = None
datablock = None
if not name:
pass
elif name in bpy.data.meshes.keys():
instance = bpy.data.meshes[name]
datablock = bpy.data.meshes[name]
elif name in bpy.data.lights.keys():
instance = bpy.data.lights[name]
datablock = bpy.data.lights[name]
elif name in bpy.data.cameras.keys():
instance = bpy.data.cameras[name]
datablock = bpy.data.cameras[name]
elif name in bpy.data.curves.keys():
instance = bpy.data.curves[name]
datablock = bpy.data.curves[name]
elif name in bpy.data.metaballs.keys():
instance = bpy.data.metaballs[name]
datablock = bpy.data.metaballs[name]
elif name in bpy.data.armatures.keys():
instance = bpy.data.armatures[name]
datablock = bpy.data.armatures[name]
elif name in bpy.data.grease_pencils.keys():
instance = bpy.data.grease_pencils[name]
datablock = bpy.data.grease_pencils[name]
elif name in bpy.data.curves.keys():
instance = bpy.data.curves[name]
datablock = bpy.data.curves[name]
elif name in bpy.data.lattices.keys():
instance = bpy.data.lattices[name]
datablock = bpy.data.lattices[name]
elif name in bpy.data.speakers.keys():
instance = bpy.data.speakers[name]
datablock = 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]
datablock = bpy.data.lightprobes[name]
else:
logging.warning(
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
elif bpy.app.version[1] >= 91 and name in bpy.data.volumes.keys():
# Only supported since 2.91
instance = bpy.data.volumes[name]
return instance
def load_data(object, name):
logging.info("loading data")
pass
datablock = bpy.data.volumes[name]
return datablock
def _is_editmode(object: bpy.types.Object) -> bool:
@@ -244,6 +247,7 @@ def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -
return dependencies
def dump_vertex_groups(src_object: bpy.types.Object) -> dict:
""" Dump object's vertex groups
@@ -289,130 +293,127 @@ def load_vertex_groups(dumped_vertex_groups: dict, target_object: bpy.types.Obje
vertex_group.add([index], weight, 'REPLACE')
class BlObject(BlDatablock):
class BlObject(ReplicatedDatablock):
bl_id = "objects"
bl_class = bpy.types.Object
bl_check_common = False
bl_icon = 'OBJECT_DATA'
bl_reload_parent = False
def _construct(self, data):
instance = None
is_root = False
if self.is_library:
with bpy.data.libraries.load(filepath=bpy.data.libraries[self.data['library']].filepath, link=True) as (sourceData, targetData):
targetData.objects = [
name for name in sourceData.objects if name == self.data['name']]
instance = bpy.data.objects[self.data['name']]
instance.uuid = self.uuid
return instance
@staticmethod
def construct(data: dict) -> bpy.types.Object:
datablock = None
# TODO: refactoring
object_name = data.get("name")
data_uuid = data.get("data_uuid")
data_id = data.get("data")
object_uuid = data.get('uuid')
object_data = get_datablock_from_uuid(
data_uuid,
find_data_from_name(data_id),
ignore=['images']) # TODO: use resolve_from_id
if object_data is None and data_uuid:
raise Exception(f"Fail to load object {data['name']}({self.uuid})")
raise Exception(f"Fail to load object {data['name']}({object_uuid})")
instance = bpy.data.objects.new(object_name, object_data)
instance.uuid = self.uuid
datablock = bpy.data.objects.new(object_name, object_data)
datablock.uuid = object_uuid
return instance
return datablock
@staticmethod
def load(data: dict, datablock: bpy.types.Object):
data = datablock.data
load_animation_data(data, datablock)
def _load_implementation(self, data, target):
loader = Loader()
data_uuid = data.get("data_uuid")
data_id = data.get("data")
if target.data and (target.data.name != data_id):
target.data = get_datablock_from_uuid(
if datablock.data and (datablock.data.name != data_id):
datablock.data = get_datablock_from_uuid(
data_uuid, find_data_from_name(data_id), ignore=['images'])
# vertex groups
vertex_groups = data.get('vertex_groups', None)
if vertex_groups:
load_vertex_groups(vertex_groups, target)
load_vertex_groups(vertex_groups, datablock)
object_data = target.data
object_data = datablock.data
# SHAPE KEYS
if 'shape_keys' in data:
target.shape_key_clear()
datablock.shape_key_clear()
# 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)
datablock.shape_key_add(name=key_block)
loader.load(
target.data.shape_keys.key_blocks[key_block], key_data)
datablock.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']
datablock.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]
datablock.data.shape_keys.key_blocks[key_block].relative_key = datablock.data.shape_keys.key_blocks[reference]
# Load transformation data
loader.load(target, data)
loader.load(datablock, data)
# Object display fields
if 'display' in data:
loader.load(target.display, data['display'])
loader.load(datablock.display, data['display'])
# Parenting
parent_id = data.get('parent_uid')
if parent_id:
parent = get_datablock_from_uuid(parent_id[0], bpy.data.objects[parent_id[1]])
# Avoid reloading
if target.parent != parent and parent is not None:
target.parent = parent
elif target.parent:
target.parent = None
if datablock.parent != parent and parent is not None:
datablock.parent = parent
elif datablock.parent:
datablock.parent = None
# Pose
if 'pose' in data:
if not target.pose:
if not datablock.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)
bg_datablock = datablock.pose.bone_groups.get(bg_name)
if not bg_target:
bg_target = target.pose.bone_groups.new(name=bg_name)
if not bg_datablock:
bg_datablock = datablock.pose.bone_groups.new(name=bg_name)
loader.load(bg_target, bg_data)
# target.pose.bone_groups.get
loader.load(bg_datablock, bg_data)
# datablock.pose.bone_groups.get
# Bones
for bone in data['pose']['bones']:
target_bone = target.pose.bones.get(bone)
datablock_bone = datablock.pose.bones.get(bone)
bone_data = data['pose']['bones'].get(bone)
if 'constraints' in bone_data.keys():
loader.load(target_bone, bone_data['constraints'])
loader.load(datablock_bone, bone_data['constraints'])
load_pose(target_bone, bone_data)
load_pose(datablock_bone, bone_data)
if 'bone_index' in bone_data.keys():
target_bone.bone_group = target.pose.bone_group[bone_data['bone_group_index']]
datablock_bone.bone_group = datablock.pose.bone_group[bone_data['bone_group_index']]
# TODO: find another way...
if target.empty_display_type == "IMAGE":
if datablock.empty_display_type == "IMAGE":
img_uuid = data.get('data_uuid')
if target.data is None and img_uuid:
target.data = get_datablock_from_uuid(img_uuid, None)
if datablock.data is None and img_uuid:
datablock.data = get_datablock_from_uuid(img_uuid, None)
if hasattr(object_data, 'skin_vertices') \
and object_data.skin_vertices\
@@ -423,20 +424,20 @@ class BlObject(BlDatablock):
skin_data.data,
SKIN_DATA)
if hasattr(target, 'cycles_visibility') \
if hasattr(datablock, 'cycles_visibility') \
and 'cycles_visibility' in data:
loader.load(target.cycles_visibility, data['cycles_visibility'])
loader.load(datablock.cycles_visibility, data['cycles_visibility'])
# TODO: handle geometry nodes input from dump_anything
if hasattr(target, 'modifiers'):
if hasattr(datablock, 'modifiers'):
nodes_modifiers = [
mod for mod in target.modifiers if mod.type == 'NODES']
mod for mod in datablock.modifiers if mod.type == 'NODES']
for modifier in nodes_modifiers:
load_modifier_geometry_node_inputs(
data['modifiers'][modifier.name], modifier)
particles_modifiers = [
mod for mod in target.modifiers if mod.type == 'PARTICLE_SYSTEM']
mod for mod in datablock.modifiers if mod.type == 'PARTICLE_SYSTEM']
for mod in particles_modifiers:
default = mod.particle_system.settings
@@ -451,34 +452,36 @@ class BlObject(BlDatablock):
bpy.data.particles.remove(default)
phys_modifiers = [
mod for mod in target.modifiers if mod.type in ['SOFT_BODY', 'CLOTH']]
mod for mod in datablock.modifiers if mod.type in ['SOFT_BODY', 'CLOTH']]
for mod in phys_modifiers:
loader.load(mod.settings, data['modifiers'][mod.name]['settings'])
# PHYSICS
load_physics(data, target)
load_physics(data, datablock)
transform = data.get('transforms', None)
if transform:
target.matrix_parent_inverse = mathutils.Matrix(
datablock.matrix_parent_inverse = mathutils.Matrix(
transform['matrix_parent_inverse'])
target.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
target.matrix_local = mathutils.Matrix(transform['matrix_local'])
datablock.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
datablock.matrix_local = mathutils.Matrix(transform['matrix_local'])
def _dump_implementation(self, data, instance=None):
assert(instance)
@staticmethod
def dump(datablock: object) -> dict:
assert(datablock)
if _is_editmode(instance):
if _is_editmode(datablock):
if self.preferences.sync_flags.sync_during_editmode:
instance.update_from_editmode()
datablock.update_from_editmode()
else:
raise ContextError("Object is in edit-mode.")
dumper = Dumper()
dumper.depth = 1
dumper.include_filter = [
"uuid",
"name",
"rotation_mode",
"data",
@@ -511,30 +514,28 @@ class BlObject(BlDatablock):
'type'
]
data = dumper.dump(instance)
data = dumper.dump(datablock)
dumper.include_filter = [
'matrix_parent_inverse',
'matrix_local',
'matrix_basis']
data['transforms'] = dumper.dump(instance)
data['transforms'] = dumper.dump(datablock)
dumper.include_filter = [
'show_shadows',
]
data['display'] = dumper.dump(instance.display)
data['display'] = dumper.dump(datablock.display)
data['data_uuid'] = getattr(instance.data, 'uuid', None)
if self.is_library:
return data
data['data_uuid'] = getattr(datablock.data, 'uuid', None)
# PARENTING
if instance.parent:
data['parent_uid'] = (instance.parent.uuid, instance.parent.name)
if datablock.parent:
data['parent_uid'] = (datablock.parent.uuid, datablock.parent.name)
# MODIFIERS
if hasattr(instance, 'modifiers'):
if hasattr(datablock, 'modifiers'):
data["modifiers"] = {}
modifiers = getattr(instance, 'modifiers', None)
modifiers = getattr(datablock, 'modifiers', None)
if modifiers:
dumper.include_filter = None
dumper.depth = 1
@@ -561,7 +562,7 @@ class BlObject(BlDatablock):
data["modifiers"][modifier.name] = dumped_modifier
gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None)
gp_modifiers = getattr(datablock, 'grease_pencil_modifiers', None)
if gp_modifiers:
dumper.include_filter = None
@@ -584,16 +585,16 @@ class BlObject(BlDatablock):
# CONSTRAINTS
if hasattr(instance, 'constraints'):
if hasattr(datablock, 'constraints'):
dumper.include_filter = None
dumper.depth = 3
data["constraints"] = dumper.dump(instance.constraints)
data["constraints"] = dumper.dump(datablock.constraints)
# POSE
if hasattr(instance, 'pose') and instance.pose:
if hasattr(datablock, 'pose') and datablock.pose:
# BONES
bones = {}
for bone in instance.pose.bones:
for bone in datablock.pose.bones:
bones[bone.name] = {}
dumper.depth = 1
rotation = 'rotation_quaternion' if bone.rotation_mode == 'QUATERNION' else 'rotation_euler'
@@ -618,7 +619,7 @@ class BlObject(BlDatablock):
# GROUPS
bone_groups = {}
for group in instance.pose.bone_groups:
for group in datablock.pose.bone_groups:
dumper.depth = 3
dumper.include_filter = [
'name',
@@ -628,11 +629,11 @@ class BlObject(BlDatablock):
data['pose']['bone_groups'] = bone_groups
# VERTEx GROUP
if len(instance.vertex_groups) > 0:
data['vertex_groups'] = dump_vertex_groups(instance)
if len(datablock.vertex_groups) > 0:
data['vertex_groups'] = dump_vertex_groups(datablock)
# SHAPE KEYS
object_data = instance.data
object_data = datablock.data
if hasattr(object_data, 'shape_keys') and object_data.shape_keys:
dumper = Dumper()
dumper.depth = 2
@@ -668,7 +669,7 @@ class BlObject(BlDatablock):
data['skin_vertices'] = skin_vertices
# CYCLE SETTINGS
if hasattr(instance, 'cycles_visibility'):
if hasattr(datablock, 'cycles_visibility'):
dumper.include_filter = [
'camera',
'diffuse',
@@ -677,36 +678,40 @@ class BlObject(BlDatablock):
'scatter',
'shadow',
]
data['cycles_visibility'] = dumper.dump(instance.cycles_visibility)
data['cycles_visibility'] = dumper.dump(
datablock.cycles_visibility)
# PHYSICS
data.update(dump_physics(instance))
return data
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: bpy.types.Object) -> list:
deps = []
# Avoid Empty case
if self.instance.data:
deps.append(self.instance.data)
if datablock.data:
deps.append(datablock.data)
# Particle systems
for particle_slot in self.instance.particle_systems:
for particle_slot in datablock.particle_systems:
deps.append(particle_slot.settings)
if self.is_library:
deps.append(self.instance.library)
if datablock.parent:
deps.append(datablock.parent)
if self.instance.parent:
deps.append(self.instance.parent)
if self.instance.instance_type == 'COLLECTION':
if datablock.instance_type == 'COLLECTION':
# TODO: uuid based
deps.append(self.instance.instance_collection)
deps.append(datablock.instance_collection)
if self.instance.modifiers:
deps.extend(find_textures_dependencies(self.instance.modifiers))
deps.extend(find_geometry_nodes_dependencies(self.instance.modifiers))
deps.extend(resolve_animation_dependencies(datablock))
if datablock.modifiers:
deps.extend(find_textures_dependencies(datablock.modifiers))
deps.extend(find_geometry_nodes_dependencies(datablock.modifiers))
return deps
_type = bpy.types.Object
_class = BlObject
@@ -23,14 +23,21 @@ import bpy
import mathutils
from deepdiff import DeepDiff
from replication.constants import DIFF_JSON, MODIFIED
from replication.protocol import ReplicatedDatablock
from replication.objects import Node
from ..utils import flush_history
from .bl_collection import (dump_collection_children, dump_collection_objects,
load_collection_childrens, load_collection_objects,
resolve_collection_dependencies)
from .bl_datablock import BlDatablock
from .bl_action import (load_animation_data,
dump_animation_data,
resolve_animation_dependencies)
from .bl_datablock import stamp_uuid
from .bl_file import get_filepath
from .dump_anything import Dumper, Loader
from ..preferences import get_preferences
RENDER_SETTINGS = [
'dither_intensity',
@@ -286,12 +293,10 @@ def dump_sequence(sequence: bpy.types.Sequence) -> dict:
dumper.depth = 1
data = dumper.dump(sequence)
# TODO: Support multiple images
if sequence.type == 'IMAGE':
data['filenames'] = [e.filename for e in sequence.elements]
# Effect strip inputs
input_count = getattr(sequence, 'input_count', None)
if input_count:
@@ -321,111 +326,116 @@ def load_sequence(sequence_data: dict, sequence_editor: bpy.types.SequenceEditor
if strip_type == 'SCENE':
strip_scene = bpy.data.scenes.get(sequence_data.get('scene'))
sequence = sequence_editor.sequences.new_scene(strip_name,
strip_scene,
strip_channel,
strip_frame_start)
strip_scene,
strip_channel,
strip_frame_start)
elif strip_type == 'MOVIE':
filepath = get_filepath(Path(sequence_data['filepath']).name)
sequence = sequence_editor.sequences.new_movie(strip_name,
filepath,
strip_channel,
strip_frame_start)
filepath,
strip_channel,
strip_frame_start)
elif strip_type == 'SOUND':
filepath = bpy.data.sounds[sequence_data['sound']].filepath
sequence = sequence_editor.sequences.new_sound(strip_name,
filepath,
strip_channel,
strip_frame_start)
filepath,
strip_channel,
strip_frame_start)
elif strip_type == 'IMAGE':
images_name = sequence_data.get('filenames')
filepath = get_filepath(images_name[0])
sequence = sequence_editor.sequences.new_image(strip_name,
filepath,
strip_channel,
strip_frame_start)
filepath,
strip_channel,
strip_frame_start)
# load other images
if len(images_name)>1:
for img_idx in range(1,len(images_name)):
if len(images_name) > 1:
for img_idx in range(1, len(images_name)):
sequence.elements.append((images_name[img_idx]))
else:
seq = {}
for i in range(sequence_data['input_count']):
seq[f"seq{i+1}"] = sequence_editor.sequences_all.get(sequence_data.get(f"input_{i+1}", None))
seq[f"seq{i+1}"] = sequence_editor.sequences_all.get(
sequence_data.get(f"input_{i+1}", None))
sequence = sequence_editor.sequences.new_effect(name=strip_name,
type=strip_type,
channel=strip_channel,
frame_start=strip_frame_start,
frame_end=sequence_data['frame_final_end'],
**seq)
type=strip_type,
channel=strip_channel,
frame_start=strip_frame_start,
frame_end=sequence_data['frame_final_end'],
**seq)
loader = Loader()
# TODO: Support filepath updates
loader.exclure_filter = ['filepath', 'sound', 'filenames','fps']
# TODO: Support filepath updates
loader.exclure_filter = ['filepath', 'sound', 'filenames', 'fps']
loader.load(sequence, sequence_data)
sequence.select = False
class BlScene(BlDatablock):
class BlScene(ReplicatedDatablock):
is_root = True
bl_id = "scenes"
bl_class = bpy.types.Scene
bl_check_common = True
bl_icon = 'SCENE_DATA'
bl_reload_parent = False
def _construct(self, data):
instance = bpy.data.scenes.new(data["name"])
instance.uuid = self.uuid
@staticmethod
def construct(data: dict) -> object:
datablock = bpy.data.scenes.new(data["name"])
datablock.uuid = data.get("uuid")
return instance
return datablock
def _load_implementation(self, data, target):
@staticmethod
def load(data: dict, datablock: object):
# Load other meshes metadata
loader = Loader()
loader.load(target, data)
loader.load(datablock, data)
# Load master collection
load_collection_objects(
data['collection']['objects'], target.collection)
data['collection']['objects'], datablock.collection)
load_collection_childrens(
data['collection']['children'], target.collection)
data['collection']['children'], datablock.collection)
if 'world' in data.keys():
target.world = bpy.data.worlds[data['world']]
datablock.world = bpy.data.worlds[data['world']]
# Annotation
if 'grease_pencil' in data.keys():
target.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']]
datablock.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']]
if self.preferences.sync_flags.sync_render_settings:
if get_preferences().sync_flags.sync_render_settings:
if 'eevee' in data.keys():
loader.load(target.eevee, data['eevee'])
loader.load(datablock.eevee, data['eevee'])
if 'cycles' in data.keys():
loader.load(target.cycles, data['cycles'])
loader.load(datablock.cycles, data['cycles'])
if 'render' in data.keys():
loader.load(target.render, data['render'])
loader.load(datablock.render, data['render'])
if 'view_settings' in data.keys():
loader.load(target.view_settings, data['view_settings'])
if target.view_settings.use_curve_mapping and \
loader.load(datablock.view_settings, data['view_settings'])
if datablock.view_settings.use_curve_mapping and \
'curve_mapping' in data['view_settings']:
# TODO: change this ugly fix
target.view_settings.curve_mapping.white_level = data[
datablock.view_settings.curve_mapping.white_level = data[
'view_settings']['curve_mapping']['white_level']
target.view_settings.curve_mapping.black_level = data[
datablock.view_settings.curve_mapping.black_level = data[
'view_settings']['curve_mapping']['black_level']
target.view_settings.curve_mapping.update()
datablock.view_settings.curve_mapping.update()
# Sequencer
sequences = data.get('sequences')
if sequences:
# Create sequencer data
target.sequence_editor_create()
vse = target.sequence_editor
datablock.sequence_editor_create()
vse = datablock.sequence_editor
# Clear removed sequences
for seq in vse.sequences_all:
@@ -435,15 +445,16 @@ class BlScene(BlDatablock):
for seq_name, seq_data in sequences.items():
load_sequence(seq_data, vse)
# If the sequence is no longer used, clear it
elif target.sequence_editor and not sequences:
target.sequence_editor_clear()
elif datablock.sequence_editor and not sequences:
datablock.sequence_editor_clear()
# FIXME: Find a better way after the replication big refacotoring
# Keep other user from deleting collection object by flushing their history
flush_history()
def _dump_implementation(self, data, instance=None):
assert(instance)
@staticmethod
def dump(datablock: object) -> dict:
stamp_uuid(datablock)
# Metadata
scene_dumper = Dumper()
@@ -456,41 +467,43 @@ class BlScene(BlDatablock):
'frame_start',
'frame_end',
'frame_step',
'uuid'
]
if self.preferences.sync_flags.sync_active_camera:
if get_preferences().sync_flags.sync_active_camera:
scene_dumper.include_filter.append('camera')
data.update(scene_dumper.dump(instance))
data = scene_dumper.dump(datablock)
dump_animation_data(datablock, data)
# Master collection
data['collection'] = {}
data['collection']['children'] = dump_collection_children(
instance.collection)
datablock.collection)
data['collection']['objects'] = dump_collection_objects(
instance.collection)
datablock.collection)
scene_dumper.depth = 1
scene_dumper.include_filter = None
# Render settings
if self.preferences.sync_flags.sync_render_settings:
if get_preferences().sync_flags.sync_render_settings:
scene_dumper.include_filter = RENDER_SETTINGS
data['render'] = scene_dumper.dump(instance.render)
data['render'] = scene_dumper.dump(datablock.render)
if instance.render.engine == 'BLENDER_EEVEE':
if datablock.render.engine == 'BLENDER_EEVEE':
scene_dumper.include_filter = EVEE_SETTINGS
data['eevee'] = scene_dumper.dump(instance.eevee)
elif instance.render.engine == 'CYCLES':
data['eevee'] = scene_dumper.dump(datablock.eevee)
elif datablock.render.engine == 'CYCLES':
scene_dumper.include_filter = CYCLES_SETTINGS
data['cycles'] = scene_dumper.dump(instance.cycles)
data['cycles'] = scene_dumper.dump(datablock.cycles)
scene_dumper.include_filter = VIEW_SETTINGS
data['view_settings'] = scene_dumper.dump(instance.view_settings)
data['view_settings'] = scene_dumper.dump(datablock.view_settings)
if instance.view_settings.use_curve_mapping:
if datablock.view_settings.use_curve_mapping:
data['view_settings']['curve_mapping'] = scene_dumper.dump(
instance.view_settings.curve_mapping)
datablock.view_settings.curve_mapping)
scene_dumper.depth = 5
scene_dumper.include_filter = [
'curves',
@@ -498,35 +511,35 @@ class BlScene(BlDatablock):
'location',
]
data['view_settings']['curve_mapping']['curves'] = scene_dumper.dump(
instance.view_settings.curve_mapping.curves)
datablock.view_settings.curve_mapping.curves)
# Sequence
vse = instance.sequence_editor
vse = datablock.sequence_editor
if vse:
dumped_sequences = {}
for seq in vse.sequences_all:
dumped_sequences[seq.name] = dump_sequence(seq)
data['sequences'] = dumped_sequences
return data
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
# Master Collection
deps.extend(resolve_collection_dependencies(self.instance.collection))
deps.extend(resolve_collection_dependencies(datablock.collection))
# world
if self.instance.world:
deps.append(self.instance.world)
if datablock.world:
deps.append(datablock.world)
# annotations
if self.instance.grease_pencil:
deps.append(self.instance.grease_pencil)
if datablock.grease_pencil:
deps.append(datablock.grease_pencil)
# Sequences
vse = self.instance.sequence_editor
vse = datablock.sequence_editor
if vse:
for sequence in vse.sequences_all:
if sequence.type == 'MOVIE' and sequence.filepath:
@@ -537,20 +550,23 @@ class BlScene(BlDatablock):
for elem in sequence.elements:
sequence.append(
Path(bpy.path.abspath(sequence.directory),
elem.filename))
elem.filename))
return deps
def diff(self):
exclude_path = []
if not self.preferences.sync_flags.sync_render_settings:
if not get_preferences().sync_flags.sync_render_settings:
exclude_path.append("root['eevee']")
exclude_path.append("root['cycles']")
exclude_path.append("root['view_settings']")
exclude_path.append("root['render']")
if not self.preferences.sync_flags.sync_active_camera:
if not get_preferences().sync_flags.sync_active_camera:
exclude_path.append("root['camera']")
return DeepDiff(self.data, self._dump(instance=self.instance), exclude_paths=exclude_path)
return DeepDiff(self.data, self._dump(datablock=self.datablock), exclude_paths=exclude_path)
_type = bpy.types.Scene
_class = BlScene
@@ -34,19 +34,19 @@ class BlSound(BlDatablock):
bl_icon = 'SOUND'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
filename = data.get('filename')
return bpy.data.sounds.load(get_filepath(filename))
def _load(self, data, target):
def load(self, data, target):
loader = Loader()
loader.load(target, data)
def diff(self):
return False
def _dump(self, instance=None):
def dump(self, instance=None):
filename = Path(instance.filepath).name
if not filename:
@@ -57,11 +57,12 @@ class BlSound(BlDatablock):
'name': instance.name
}
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
if self.instance.filepath and self.instance.filepath != '<builtin>':
ensure_unpacked(self.instance)
if datablock.filepath and datablock.filepath != '<builtin>':
ensure_unpacked(datablock)
deps.append(Path(bpy.path.abspath(self.instance.filepath)))
deps.append(Path(bpy.path.abspath(datablock.filepath)))
return deps
@@ -30,14 +30,14 @@ class BlSpeaker(BlDatablock):
bl_icon = 'SPEAKER'
bl_reload_parent = False
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.speakers.new(data["name"])
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
dumper = Dumper()
@@ -60,11 +60,11 @@ class BlSpeaker(BlDatablock):
return dumper.dump(instance)
def _resolve_deps_implementation(self):
# TODO: resolve material
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
sound = self.instance.sound
sound = datablock.sound
if sound:
deps.append(sound)
@@ -30,14 +30,14 @@ class BlTexture(BlDatablock):
bl_icon = 'TEXTURE'
bl_reload_parent = False
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.textures.new(data["name"], data["type"])
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
dumper = Dumper()
@@ -61,11 +61,11 @@ class BlTexture(BlDatablock):
return data
def _resolve_deps_implementation(self):
# TODO: resolve material
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
image = getattr(self.instance,"image", None)
image = getattr(datablock,"image", None)
if image:
deps.append(image)
@@ -31,7 +31,7 @@ class BlVolume(BlDatablock):
bl_icon = 'VOLUME_DATA'
bl_reload_parent = False
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
loader.load(target.display, data['display'])
@@ -41,10 +41,10 @@ class BlVolume(BlDatablock):
if src_materials:
load_materials_slots(src_materials, target.materials)
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.volumes.new(data["name"])
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
dumper = Dumper()
@@ -69,15 +69,15 @@ class BlVolume(BlDatablock):
return data
def _resolve_deps_implementation(self):
# TODO: resolve material
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
external_vdb = Path(bpy.path.abspath(self.instance.filepath))
external_vdb = Path(bpy.path.abspath(datablock.filepath))
if external_vdb.exists() and not external_vdb.is_dir():
deps.append(external_vdb)
for material in self.instance.materials:
for material in datablock.materials:
if material:
deps.append(material)
@@ -33,10 +33,10 @@ class BlWorld(BlDatablock):
bl_icon = 'WORLD_DATA'
bl_reload_parent = False
def _construct(self, data):
def construct(data: dict) -> object:
return bpy.data.worlds.new(data["name"])
def _load_implementation(self, data, target):
def load(data: dict, datablock: object):
loader = Loader()
loader.load(target, data)
@@ -46,7 +46,7 @@ class BlWorld(BlDatablock):
load_node_tree(data['node_tree'], target.node_tree)
def _dump_implementation(self, data, instance=None):
def dump(datablock: object) -> dict:
assert(instance)
world_dumper = Dumper()
@@ -62,11 +62,11 @@ class BlWorld(BlDatablock):
return data
def _resolve_deps_implementation(self):
@staticmethod
def resolve_deps(datablock: object) -> [object]:
deps = []
if self.instance.use_nodes:
deps.extend(get_node_tree_dependencies(self.instance.node_tree))
if self.is_library:
deps.append(self.instance.library)
if datablock.use_nodes:
deps.extend(get_node_tree_dependencies(datablock.node_tree))
return deps
+123 -503
View File
@@ -32,7 +32,7 @@ from operator import itemgetter
from pathlib import Path
from queue import Queue
from time import gmtime, strftime
from numpy import interp
import traceback
try:
import _pickle as pickle
@@ -40,17 +40,18 @@ except ImportError:
import pickle
import bpy
import bmesh
import mathutils
from bpy.app.handlers import persistent
from bpy_extras.io_utils import ExportHelper, ImportHelper
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
STATE_INITIAL, STATE_SYNCING, UP)
from replication.data import ReplicatedDataFactory
from replication.exception import NonAuthorizedOperationError, ContextError
from replication.protocol import DataTranslationProtocol
from replication.exception import ContextError, NonAuthorizedOperationError
from replication.interface import session
from replication import porcelain
from replication.repository import Repository
from . import bl_types, environment, timers, ui, utils
from . import io_bpy, environment, timers, ui, utils
from .presence import SessionStatusWidget, renderer, view3d_find
from .timers import registry
@@ -58,225 +59,6 @@ background_execution_queue = Queue()
deleyables = []
stop_modal_executor = False
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
'collections', 'curves', 'fonts',
'grease_pencils', 'images', 'lattices', 'libraries',
'lightprobes', 'lights', 'linestyles', 'masks',
'materials', 'meshes', 'metaballs', 'movieclips',
'node_groups', 'objects', 'paint_curves', 'particles',
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
'textures', 'volumes', 'worlds']
PERSISTENT_DATABLOCKS = ['LineStyle', 'Dots Stroke', 'replay_action']
def clean_scene(ignored_datablocks: list = None):
"""
Delete all datablock of the scene except PERSISTENT_DATABLOCKS and ignored
ones in ignored_datablocks.
"""
PERSISTENT_DATABLOCKS.extend(ignored_datablocks)
# Avoid to trigger a runtime error by keeping the last scene
PERSISTENT_DATABLOCKS.append(bpy.data.scenes[0].name)
for type_name in CLEARED_DATABLOCKS:
type_collection = getattr(bpy.data, type_name)
for datablock in type_collection:
if datablock.name in PERSISTENT_DATABLOCKS:
logging.debug(f"Skipping {datablock.name}")
continue
else:
logging.debug(f"Removing {datablock.name}")
type_collection.remove(datablock)
# Clear sequencer
bpy.context.scene.sequence_editor_clear()
def get_bb_coords_from_obj(object: bpy.types.Object, instance: bpy.types.Object = None) -> list:
""" Generate bounding box in world coordinate from object bound box
:param object: target object
:type object: bpy.types.Object
:param instance: optionnal instance
:type instance: bpy.types.Object
:return: list of 8 points [(x,y,z),...]
"""
base = object.matrix_world
if instance:
scale = mathutils.Matrix.Diagonal(object.matrix_world.to_scale())
base = instance.matrix_world @ scale.to_4x4()
bbox_corners = [base @ mathutils.Vector(
corner) for corner in object.bound_box]
return [(point.x, point.y, point.z) for point in bbox_corners]
def bbox_from_obj(obj: bpy.types.Object, index: int = 1) -> list:
""" Generate a bounding box for a given object by using its world matrix
:param obj: target object
:type obj: bpy.types.Object
:param index: indice offset
:type index: int
:return: list of 8 points [(x,y,z),...], list of 12 link between these points [(1,2),...]
"""
radius = 1.0 # Radius of the bounding box
index = 8*index
vertex_indices = (
(0+index, 1+index), (0+index, 2+index), (1+index, 3+index), (2+index, 3+index),
(4+index, 5+index), (4+index, 6+index), (5+index, 7+index), (6+index, 7+index),
(0+index, 4+index), (1+index, 5+index), (2+index, 6+index), (3+index, 7+index))
if obj.type == 'EMPTY':
radius = obj.empty_display_size
elif obj.type == 'LIGHT':
radius = obj.data.shadow_soft_size
elif obj.type == 'LIGHT_PROBE':
radius = obj.data.influence_distance
elif obj.type == 'CAMERA':
radius = obj.data.display_size
elif hasattr(obj, 'bound_box'):
vertex_indices = (
(0+index, 1+index), (1+index, 2+index),
(2+index, 3+index), (0+index, 3+index),
(4+index, 5+index), (5+index, 6+index),
(6+index, 7+index), (4+index, 7+index),
(0+index, 4+index), (1+index, 5+index),
(2+index, 6+index), (3+index, 7+index))
vertex_pos = get_bb_coords_from_obj(obj)
return vertex_pos, vertex_indices
coords = [
(-radius, -radius, -radius), (+radius, -radius, -radius),
(-radius, +radius, -radius), (+radius, +radius, -radius),
(-radius, -radius, +radius), (+radius, -radius, +radius),
(-radius, +radius, +radius), (+radius, +radius, +radius)]
base = obj.matrix_world
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
return vertex_pos, vertex_indices
def draw_user(username, metadata, radius=0.01, intensity=10.0):
"""
Generate a mesh representation of a given user frustum and
sight of view.
"""
view_corners = metadata.get('view_corners')
color = metadata.get('color', (1,1,1,0))
objects = metadata.get('selected_objects', None)
scene = metadata.get('scene_current', bpy.context.scene.name)
user_collection = bpy.data.collections.new(username)
# User Color
user_mat = bpy.data.materials.new(username)
user_mat.use_nodes = True
nodes = user_mat.node_tree.nodes
nodes.remove(nodes['Principled BSDF'])
emission_node = nodes.new('ShaderNodeEmission')
emission_node.inputs['Color'].default_value = color
emission_node.inputs['Strength'].default_value = intensity
output_node = nodes['Material Output']
user_mat.node_tree.links.new(
emission_node.outputs['Emission'], output_node.inputs['Surface'])
# Generate camera mesh
camera_vertices = view_corners[:4]
camera_vertices.append(view_corners[6])
camera_mesh = bpy.data.meshes.new(f"{username}_camera")
camera_obj = bpy.data.objects.new(f"{username}_camera", camera_mesh)
frustum_bm = bmesh.new()
frustum_bm.from_mesh(camera_mesh)
for p in camera_vertices:
frustum_bm.verts.new(p)
frustum_bm.verts.ensure_lookup_table()
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[2]))
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[1]))
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[3]))
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[0]))
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[4]))
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[4]))
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[4]))
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[4]))
frustum_bm.edges.ensure_lookup_table()
frustum_bm.to_mesh(camera_mesh)
frustum_bm.free() # free and prevent further access
camera_obj.modifiers.new("wireframe", "SKIN")
camera_obj.data.skin_vertices[0].data[0].use_root = True
for v in camera_mesh.skin_vertices[0].data:
v.radius = [radius, radius]
camera_mesh.materials.append(user_mat)
user_collection.objects.link(camera_obj)
# Generate sight mesh
sight_mesh = bpy.data.meshes.new(f"{username}_sight")
sight_obj = bpy.data.objects.new(f"{username}_sight", sight_mesh)
sight_verts = view_corners[4:6]
sight_bm = bmesh.new()
sight_bm.from_mesh(sight_mesh)
for p in sight_verts:
sight_bm.verts.new(p)
sight_bm.verts.ensure_lookup_table()
sight_bm.edges.new((sight_bm.verts[0], sight_bm.verts[1]))
sight_bm.edges.ensure_lookup_table()
sight_bm.to_mesh(sight_mesh)
sight_bm.free()
sight_obj.modifiers.new("wireframe", "SKIN")
sight_obj.data.skin_vertices[0].data[0].use_root = True
for v in sight_mesh.skin_vertices[0].data:
v.radius = [radius, radius]
sight_mesh.materials.append(user_mat)
user_collection.objects.link(sight_obj)
# Draw selected objects
if objects:
for o in list(objects):
instance = bl_types.bl_datablock.get_datablock_from_uuid(o, None)
if instance:
bbox_mesh = bpy.data.meshes.new(f"{instance.name}_bbox")
bbox_obj = bpy.data.objects.new(
f"{instance.name}_bbox", bbox_mesh)
bbox_verts, bbox_ind = bbox_from_obj(instance, index=0)
bbox_bm = bmesh.new()
bbox_bm.from_mesh(bbox_mesh)
for p in bbox_verts:
bbox_bm.verts.new(p)
bbox_bm.verts.ensure_lookup_table()
for e in bbox_ind:
bbox_bm.edges.new(
(bbox_bm.verts[e[0]], bbox_bm.verts[e[1]]))
bbox_bm.to_mesh(bbox_mesh)
bbox_bm.free()
bpy.data.collections[username].objects.link(bbox_obj)
bbox_obj.modifiers.new("wireframe", "SKIN")
bbox_obj.data.skin_vertices[0].data[0].use_root = True
for v in bbox_mesh.skin_vertices[0].data:
v.radius = [radius, radius]
bbox_mesh.materials.append(user_mat)
bpy.data.scenes[scene].collection.children.link(user_collection)
def session_callback(name):
""" Session callback wrapper
@@ -302,17 +84,17 @@ def initialize_session():
# Step 1: Constrect nodes
logging.info("Constructing nodes")
for node in session._graph.list_ordered():
node_ref = session.get(uuid=node)
for node in session.repository.list_ordered():
node_ref = session.repository.get_node(node)
if node_ref is None:
logging.error(f"Can't construct node {node}")
elif node_ref.state == FETCHED:
node_ref.resolve()
# Step 2: Load nodes
logging.info("Loading nodes")
for node in session._graph.list_ordered():
node_ref = session.get(uuid=node)
for node in session.repository.list_ordered():
node_ref = session.repository.get_node(node)
if node_ref is None:
logging.error(f"Can't load node {node}")
@@ -359,7 +141,8 @@ def on_connection_end(reason="none"):
if isinstance(handler, logging.FileHandler):
logger.removeHandler(handler)
if reason != "user":
bpy.ops.session.notify('INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ")
bpy.ops.session.notify(
'INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ")
# OPERATORS
@@ -408,109 +191,77 @@ class SessionStartOperator(bpy.types.Operator):
handler.setFormatter(formatter)
bpy_factory = ReplicatedDataFactory()
supported_bl_types = []
bpy_protocol = io_bpy.get_data_translation_protocol()
# init the factory with supported types
for type in bl_types.types_to_register():
type_module = getattr(bl_types, type)
name = [e.capitalize() for e in type.split('_')[1:]]
type_impl_name = 'Bl'+''.join(name)
type_module_class = getattr(type_module, type_impl_name)
supported_bl_types.append(type_module_class.bl_id)
if type_impl_name not in settings.supported_datablocks:
logging.info(f"{type_impl_name} not found, \
# Check if supported_datablocks are up to date before starting the
# the session
for impl in bpy_protocol.implementations.values():
if impl.__name__ not in settings.supported_datablocks:
logging.info(f"{impl.__name__} not found, \
regenerate type settings...")
settings.generate_supported_types()
type_local_config = settings.supported_datablocks[type_impl_name]
bpy_factory.register_type(
type_module_class.bl_class,
type_module_class,
check_common=type_module_class.bl_check_common)
# Ensure blender 2.8 compatibility
if bpy.app.version[1] >= 91:
python_binary_path = sys.executable
else:
python_binary_path = bpy.app.binary_path_python
session.configure(
factory=bpy_factory,
python_path=python_binary_path,
external_update_handling=True)
# Host a session
# HOST
if self.host:
if settings.init_method == 'EMPTY':
utils.clean_scene()
runtime_settings.is_host = True
runtime_settings.internet_ip = environment.get_ip()
# Start the server locally
server = porcelain.serve(port=settings.port,
timeout=settings.connection_timeout,
admin_password=admin_pass,
log_directory=settings.cache_directory)
try:
for scene in bpy.data.scenes:
session.add(scene)
# Init repository
repo = porcelain.init(bare=False,
data_protocol=bpy_protocol)
session.host(
id=settings.username,
port=settings.port,
ipc_port=settings.ipc_port,
timeout=settings.connection_timeout,
password=admin_pass,
cache_directory=settings.cache_directory,
server_log_level=logging.getLevelName(
logging.getLogger().level),
)
except Exception as e:
self.report({'ERROR'}, repr(e))
logging.error(f"Error: {e}")
import traceback
traceback.print_exc()
# Join a session
# Add the existing scenes
for scene in bpy.data.scenes:
porcelain.add(repo, scene)
porcelain.remote_add(repo,
'server',
'127.0.0.1',
settings.port)
porcelain.sync(repo, 'server')
porcelain.push(repo, 'server')
# JOIN
else:
if not runtime_settings.admin:
utils.clean_scene()
# regular session, no password needed
admin_pass = None
utils.clean_scene()
try:
session.connect(
id=settings.username,
address=settings.ip,
port=settings.port,
ipc_port=settings.ipc_port,
timeout=settings.connection_timeout,
password=admin_pass
)
except Exception as e:
self.report({'ERROR'}, str(e))
logging.error(str(e))
repo = porcelain.clone(settings.ip, settings.ip)
# Background client updates service
deleyables.append(timers.ClientUpdate())
deleyables.append(timers.DynamicRightSelectTimer())
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
# deleyables.append(timers.ClientUpdate())
# deleyables.append(timers.DynamicRightSelectTimer())
# deleyables.append(timers.ApplyTimer(
# timeout=settings.depsgraph_update_rate))
# deleyables.append(timers.PushTimer(
# queue=stagging,
# timeout=settings.depsgraph_update_rate
# ))
session_update = timers.SessionStatusUpdate()
session_user_sync = timers.SessionUserSync()
session_background_executor = timers.MainThreadExecutor(
execution_queue=background_execution_queue)
# session_update = timers.SessionStatusUpdate()
# session_user_sync = timers.SessionUserSync()
# session_background_executor = timers.MainThreadExecutor(
# execution_queue=background_execution_queue)
# session_listen = timers.SessionListenTimer(timeout=0.001)
session_update.register()
session_user_sync.register()
session_background_executor.register()
# session_listen.register()
# session_update.register()
# session_user_sync.register()
# session_background_executor.register()
deleyables.append(session_background_executor)
deleyables.append(session_update)
deleyables.append(session_user_sync)
# deleyables.append(session_background_executor)
# deleyables.append(session_update)
# deleyables.append(session_user_sync)
# deleyables.append(session_listen)
self.report(
{'INFO'},
@@ -550,7 +301,7 @@ class SessionInitOperator(bpy.types.Operator):
utils.clean_scene()
for scene in bpy.data.scenes:
session.add(scene)
porcelain.add(session.repository, scene)
session.init()
@@ -572,7 +323,7 @@ class SessionStopOperator(bpy.types.Operator):
if session:
try:
session.disconnect()
session.disconnect(reason='user')
except Exception as e:
self.report({'ERROR'}, repr(e))
@@ -821,17 +572,22 @@ class SessionApply(bpy.types.Operator):
def execute(self, context):
logging.debug(f"Running apply on {self.target}")
try:
node_ref = session.get(uuid=self.target)
session.apply(self.target,
force=True,
force_dependencies=self.reset_dependencies)
node_ref = session.repository.get_node(self.target)
porcelain.apply(session.repository,
self.target,
force=True,
force_dependencies=self.reset_dependencies)
if node_ref.bl_reload_parent:
for parent in session._graph.find_parents(self.target):
for parent in session.repository.get_parents(self.target):
logging.debug(f"Refresh parent {parent}")
session.apply(parent, force=True)
porcelain.apply(session.repository,
parent.uuid,
force=True)
except Exception as e:
self.report({'ERROR'}, repr(e))
return {"CANCELED"}
traceback.print_exc()
return {"CANCELLED"}
return {"FINISHED"}
@@ -850,13 +606,14 @@ class SessionCommit(bpy.types.Operator):
def execute(self, context):
try:
session.commit(uuid=self.target)
porcelain.commit(session.repository, uuid=self.target)
session.push(self.target)
return {"FINISHED"}
except Exception as e:
self.report({'ERROR'}, repr(e))
return {"CANCELED"}
class ApplyArmatureOperator(bpy.types.Operator):
"""Operator which runs its self from a timer"""
bl_idname = "session.apply_armature_operator"
@@ -871,15 +628,15 @@ class ApplyArmatureOperator(bpy.types.Operator):
return {'CANCELLED'}
if event.type == 'TIMER':
if session and session.state['STATE'] == STATE_ACTIVE:
nodes = session.list(filter=bl_types.bl_armature.BlArmature)
if session and session.state == STATE_ACTIVE:
nodes = session.list(filter=io_bpy.bl_armature.BlArmature)
for node in nodes:
node_ref = session.get(uuid=node)
node_ref = session.repository.get_node(node)
if node_ref.state == FETCHED:
try:
session.apply(node)
porcelain.apply(session.repository, node)
except Exception as e:
logging.error("Fail to apply armature: {e}")
@@ -928,6 +685,7 @@ class SessionClearCache(bpy.types.Operator):
row = self.layout
row.label(text=f" Do you really want to remove local cache ? ")
class SessionPurgeOperator(bpy.types.Operator):
"Remove node with lost references"
bl_idname = "session.purge"
@@ -972,7 +730,6 @@ class SessionNotifyOperator(bpy.types.Operator):
layout = self.layout
layout.row().label(text=self.message)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -1016,7 +773,8 @@ class SessionSaveBackupOperator(bpy.types.Operator, ExportHelper):
@classmethod
def poll(cls, context):
return session.state['STATE'] == STATE_ACTIVE
return session.state == STATE_ACTIVE
class SessionStopAutoSaveOperator(bpy.types.Operator):
bl_idname = "session.cancel_autosave"
@@ -1025,7 +783,7 @@ class SessionStopAutoSaveOperator(bpy.types.Operator):
@classmethod
def poll(cls, context):
return (session.state['STATE'] == STATE_ACTIVE and 'SessionBackupTimer' in registry)
return (session.state == STATE_ACTIVE and 'SessionBackupTimer' in registry)
def execute(self, context):
autosave_timer = registry.get('SessionBackupTimer')
@@ -1033,26 +791,6 @@ class SessionStopAutoSaveOperator(bpy.types.Operator):
return {'FINISHED'}
class SessionRenderReplay(bpy.types.Operator):
bl_idname = "session.render_replay"
bl_label = "Render Replay"
bl_description = "Render Replay"
@classmethod
def poll(cls, context):
return context.window_manager.session.replay_files
def execute(self, context):
base_path = str(context.scene.render.filepath)
for frame in range(0,context.scene.frame_end):
logging.info(f"Rendering frame {frame} to {base_path}_{frame}.png")
context.scene.frame_current = frame
filename = Path(bpy.context.window_manager.session.replay_files[context.scene.active_replay_file].name)
context.scene.render.filepath = f"{base_path}{frame}_{filename.stem}"
bpy.ops.render.render(write_still=True)
context.scene.render.filepath = base_path
return {'FINISHED'}
class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
bl_idname = "session.load"
@@ -1069,43 +807,8 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
maxlen=255, # Max internal buffer length, longer would be clamped.
)
draw_users: bpy.props.BoolProperty(
name="Load users",
description="Draw users in the scene",
default=False,
)
replay: bpy.props.BoolProperty(
name="Replay mode",
description="Enable replay functions",
default=False,
)
user_skin_radius: bpy.props.FloatProperty(
name="Wireframe radius",
description="Wireframe radius",
default=0.005,
)
user_color_intensity: bpy.props.FloatProperty(
name="Shading intensity",
description="Shading intensity",
default=1.0,
)
files: bpy.props.CollectionProperty(
name='File paths',
type=bpy.types.OperatorFileListElement
)
def draw(self, context):
pass
def execute(self, context):
from replication.graph import ReplicationGraph
runtime_settings = context.window_manager.session
# TODO: add filechecks
from replication.repository import Repository
try:
f = gzip.open(self.filepath, "rb")
@@ -1113,34 +816,31 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
except OSError as e:
f = open(self.filepath, "rb")
db = pickle.load(f)
if db:
logging.info(f"Reading {self.filepath}")
nodes = db.get("nodes")
logging.info(f"{len(nodes)} Nodes to load")
# init the factory with supported types
bpy_factory = ReplicatedDataFactory()
for type in bl_types.types_to_register():
type_module = getattr(bl_types, type)
bpy_protocol = DataTranslationProtocol()
for type in io_bpy.types_to_register():
type_module = getattr(io_bpy, type)
name = [e.capitalize() for e in type.split('_')[1:]]
type_impl_name = 'Bl'+''.join(name)
type_module_class = getattr(type_module, type_impl_name)
bpy_factory.register_type(
bpy_protocol.register_type(
type_module_class.bl_class,
type_module_class)
graph = ReplicationGraph()
graph = Repository()
for node, node_data in nodes:
node_type = node_data.get('str_type')
impl = bpy_factory.get_implementation_from_net(node_type)
impl = bpy_protocol.get_implementation_from_net(node_type)
if impl:
logging.info(f"Loading {node}")
@@ -1148,21 +848,12 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
uuid=node,
dependencies=node_data['dependencies'],
data=node_data['data'])
instance.store(graph)
graph.do_commit(instance)
instance.state = FETCHED
logging.info("Graph succefully loaded")
# Persitstent collection
ignored_datablocks = []
persistent_collection = bpy.data.collections.get("multiuser_timelapse")
if self.replay and \
runtime_settings.replay_persistent_collection and \
persistent_collection:
ignored_datablocks = ['multiuser_timelapse','multiuser_timelapse_cam','multiuser_timelapse_cam_obj','multiuser_timelapse_path','multiuser_timelapse_path_obj', 'multiuser_timelapse_pathAction']
clean_scene(ignored_datablocks=ignored_datablocks)
utils.clean_scene()
# Step 1: Construct nodes
for node in graph.list_ordered():
@@ -1172,78 +863,16 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
for node in graph.list_ordered():
graph[node].apply()
if len(self.files) > 1:
runtime_settings.replay_files.clear()
context.scene.active_replay_file = len(self.files)-1
directory = Path(self.filepath).parent
file_list = [f['name'] for f in self.files]
file_list.sort()
for f in file_list:
snap = runtime_settings.replay_files.add()
snap.name = str(Path(directory, f))
print(f)
if runtime_settings.replay_mode == 'TIMELINE':
replay_action = bpy.data.actions.get('replay_action', bpy.data.actions.new('replay_action'))
bpy.context.scene.animation_data_create()
bpy.context.scene.animation_data.action = replay_action
if len(replay_action.fcurves) > 0 and replay_action.fcurves[0].data_path == 'active_replay_file':
replay_fcurve = replay_action.fcurves[0]
else:
replay_fcurve = replay_action.fcurves.new('active_replay_file')
for p in reversed(replay_fcurve.keyframe_points):
replay_fcurve.keyframe_points.remove(p, fast=True)
duration = runtime_settings.replay_duration
file_count = len(self.files)-1
for index in range(0, file_count):
frame = interp(index, [0, file_count], [bpy.context.scene.frame_start, duration])
replay_fcurve.keyframe_points.insert(frame, index)
if self.draw_users:
f = gzip.open(self.filepath, "rb")
db = pickle.load(f)
users = db.get("users")
for username, user_data in users.items():
metadata = user_data['metadata']
if metadata:
draw_user(username, metadata, radius=self.user_skin_radius, intensity=self.user_color_intensity)
# Relink the persistent collection
if self.replay and persistent_collection:
logging.info(f"Relinking {persistent_collection.name}")
bpy.context.scene.collection.children.link(persistent_collection)
# Reasign scene action
if self.replay and \
runtime_settings.replay_mode == 'TIMELINE' and \
not bpy.context.scene.animation_data :
bpy.context.scene.animation_data_create()
bpy.context.scene.animation_data.action = bpy.data.actions.get('replay_action')
bpy.context.scene.frame_end = runtime_settings.replay_duration
# Reasign the scene camera
if self.replay and \
runtime_settings.replay_persistent_collection and \
runtime_settings.replay_camera:
bpy.context.scene.camera = runtime_settings.replay_camera
bpy.context.scene.eevee.use_bloom = False
return {'FINISHED'}
@classmethod
def poll(cls, context):
return True
def menu_func_import(self, context):
self.layout.operator(SessionLoadSaveOperator.bl_idname, text='Multi-user session snapshot (.db)')
self.layout.operator(SessionLoadSaveOperator.bl_idname,
text='Multi-user session snapshot (.db)')
classes = (
@@ -1264,26 +893,27 @@ classes = (
SessionLoadSaveOperator,
SessionStopAutoSaveOperator,
SessionPurgeOperator,
SessionRenderReplay
)
def update_external_dependencies():
nodes_ids = session.list(filter=bl_types.bl_file.BlFile)
nodes_ids = session.list(filter=io_bpy.bl_file.BlFile)
for node_id in nodes_ids:
node = session.get(node_id)
node = session.repository.get_node(node_id)
if node and node.owner in [session.id, RP_COMMON] \
and node.has_changed():
session.commit(node_id)
porcelain.commit(session.repository, node_id)
session.push(node_id, check_data=False)
def sanitize_deps_graph(remove_nodes: bool = False):
""" Cleanup the replication graph
"""
if session and session.state['STATE'] == STATE_ACTIVE:
if session and session.state == STATE_ACTIVE:
start = utils.current_milli_time()
rm_cpt = 0
for node_key in session.list():
node = session.get(node_key)
node = session.repository.get_node(node_key)
if node is None \
or (node.state == UP and not node.resolve(construct=False)):
if remove_nodes:
@@ -1304,28 +934,19 @@ def resolve_deps_graph(dummy):
A future solution should be to avoid storing dataclock reference...
"""
if session and session.state['STATE'] == STATE_ACTIVE:
if session and session.state == STATE_ACTIVE:
sanitize_deps_graph(remove_nodes=True)
@persistent
def load_pre_handler(dummy):
if session and session.state['STATE'] in [STATE_ACTIVE, STATE_SYNCING]:
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
bpy.ops.session.stop()
@persistent
def update_client_frame(scene):
setting = bpy.context.window_manager.session
if setting.replay_mode == 'TIMELINE' and \
setting.replay_files and \
scene.active_replay_file != setting.replay_frame_current :
index = bpy.context.scene.active_replay_file
bpy.ops.session.load(filepath=bpy.context.window_manager.session.replay_files[index].name,
draw_users=True,
replay=True)
setting.replay_frame_current = index
if session and session.state['STATE'] == STATE_ACTIVE:
if session and session.state == STATE_ACTIVE:
session.update_user_metadata({
'frame_current': scene.frame_current
})
@@ -1333,7 +954,7 @@ def update_client_frame(scene):
@persistent
def depsgraph_evaluation(scene):
if session and session.state['STATE'] == STATE_ACTIVE:
if session and session.state == STATE_ACTIVE:
context = bpy.context
blender_depsgraph = bpy.context.view_layer.depsgraph
dependency_updates = [u for u in blender_depsgraph.updates]
@@ -1346,8 +967,8 @@ def depsgraph_evaluation(scene):
# Is the object tracked ?
if update.id.uuid:
# Retrieve local version
node = session.get(uuid=update.id.uuid)
node = session.repository.get_node(update.id.uuid)
# Check our right on this update:
# - if its ours or ( under common and diff), launch the
# update process
@@ -1356,34 +977,33 @@ def depsgraph_evaluation(scene):
if node.state == UP:
try:
if node.has_changed():
session.commit(node.uuid)
porcelain.commit(session.repository, node.uuid)
session.push(node.uuid, check_data=False)
except ReferenceError:
logging.debug(f"Reference error {node.uuid}")
if not node.is_valid():
session.remove(node.uuid)
except ContextError as e:
logging.debug(e)
logging.debug(e)
except Exception as e:
logging.error(e)
else:
continue
# A new scene is created
# A new scene is created
elif isinstance(update.id, bpy.types.Scene):
ref = session.get(reference=update.id)
ref = session.repository.get_node_by_datablock(update.id)
if ref:
ref.resolve()
else:
scn_uuid = session.add(update.id)
session.commit(scn_uuid)
session.push(scn_uuid, check_data=False)
scn_uuid = porcelain.add(session.repository, update.id)
porcelain.commit(session.repository, scn_uuid)
porcelain.push(session.repository)
def register():
from bpy.utils import register_class
for cls in classes:
for cls in classes:
register_class(cls)
bpy.app.handlers.undo_post.append(resolve_deps_graph)
bpy.app.handlers.redo_post.append(resolve_deps_graph)
@@ -1392,7 +1012,7 @@ def register():
def unregister():
if session and session.state['STATE'] == STATE_ACTIVE:
if session and session.state == STATE_ACTIVE:
session.disconnect()
from bpy.utils import unregister_class
+13 -158
View File
@@ -24,11 +24,11 @@ import os
from pathlib import Path
from . import bl_types, environment, addon_updater_ops, presence, ui
from . import io_bpy, environment, addon_updater_ops, presence, ui
from .utils import get_preferences, get_expanded_icon
from replication.constants import RP_COMMON
from replication.interface import session
from numpy import interp
# From https://stackoverflow.com/a/106223
IP_REGEX = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")
@@ -66,14 +66,6 @@ def update_ip(self, context):
self['ip'] = "127.0.0.1"
def update_port(self, context):
max_port = self.port + 3
if self.ipc_port < max_port and \
self['ipc_port'] >= self.port:
logging.error(
"IPC Port in conflict with the port, assigning a random value")
self['ipc_port'] = random.randrange(self.port+4, 10000)
def update_directory(self, context):
@@ -93,88 +85,6 @@ def set_log_level(self, value):
def get_log_level(self):
return logging.getLogger().level
def set_active_replay(self, value):
files_count = len(bpy.context.window_manager.session.replay_files)
if files_count == 0:
return
max_index = files_count-1
if value > max_index:
value = max_index
if hasattr(self, 'active_replay_file'):
self["active_replay_file"] = value
else:
self.active_replay_file = value
if bpy.context.window_manager.session.replay_mode == 'MANUAL':
bpy.ops.session.load(
filepath=bpy.context.window_manager.session.replay_files[value].name,
draw_users=True,
replay=True)
def get_active_replay(self):
return self.get('active_replay_file', 0)
def set_replay_persistent_collection(self, value):
if hasattr(self, 'replay_persistent_collection'):
self["replay_persistent_collection"] = value
else:
self.replay_persistent_collection = value
collection = bpy.data.collections.get("multiuser_timelapse", None)
if collection is None and value:
collection = bpy.data.collections.new('multiuser_timelapse')
cam = bpy.data.cameras.get('multiuser_timelapse_cam', bpy.data.cameras.new('multiuser_timelapse_cam'))
cam_obj = bpy.data.objects.get('multiuser_timelapse_cam_obj', bpy.data.objects.new('multiuser_timelapse_cam_obj', cam))
curve = bpy.data.curves.get('multiuser_timelapse_path', bpy.data.curves.new('multiuser_timelapse_path', 'CURVE'))
curve_obj = bpy.data.objects.get('multiuser_timelapse_path_obj', bpy.data.objects.new('multiuser_timelapse_path_obj', curve))
if cam_obj.name not in collection.objects:
collection.objects.link(cam_obj)
if curve_obj.name not in collection.objects:
collection.objects.link(curve_obj)
bpy.context.scene.collection.children.link(collection)
elif collection and not value:
for o in collection.objects:
bpy.data.objects.remove(o)
bpy.data.collections.remove(collection)
def get_replay_persistent_collection(self):
return self.get('replay_persistent_collection', False)
def set_replay_duration(self, value):
if hasattr(self, 'replay_duration'):
self["replay_duration"] = value
else:
self.replay_duration = value
# Update the animation fcurve
replay_action = bpy.data.actions.get('replay_action')
replay_fcurve = None
for fcurve in replay_action.fcurves:
if fcurve.data_path == 'active_replay_file':
replay_fcurve = fcurve
if replay_fcurve:
for p in reversed(replay_fcurve.keyframe_points):
replay_fcurve.keyframe_points.remove(p, fast=True)
bpy.context.scene.frame_end = value
files_count = len(bpy.context.window_manager.session.replay_files)-1
for index in range(0, files_count):
frame = interp(index,[0, files_count],[bpy.context.scene.frame_start, value])
replay_fcurve.keyframe_points.insert(frame, index)
def get_replay_duration(self):
return self.get('replay_duration', 10)
class ReplicatedDatablock(bpy.types.PropertyGroup):
type_name: bpy.props.StringProperty()
@@ -256,12 +166,6 @@ class SessionPrefs(bpy.types.AddonPreferences):
supported_datablocks: bpy.props.CollectionProperty(
type=ReplicatedDatablock,
)
ipc_port: bpy.props.IntProperty(
name="ipc_port",
description='internal ttl port(only useful for multiple local instances)',
default=random.randrange(5570, 70000),
update=update_port,
)
init_method: bpy.props.EnumProperty(
name='init_method',
description='Init repo',
@@ -503,18 +407,17 @@ class SessionPrefs(bpy.types.AddonPreferences):
def generate_supported_types(self):
self.supported_datablocks.clear()
for type in bl_types.types_to_register():
bpy_protocol = io_bpy.get_data_translation_protocol()
# init the factory with supported types
for impl in bpy_protocol.implementations.values():
new_db = self.supported_datablocks.add()
type_module = getattr(bl_types, type)
name = [e.capitalize() for e in type.split('_')[1:]]
type_impl_name = 'Bl'+''.join(name)
type_module_class = getattr(type_module, type_impl_name)
new_db.name = type_impl_name
new_db.type_name = type_impl_name
new_db.name = impl.__name__
new_db.type_name = impl.__name__
new_db.use_as_filter = True
new_db.icon = type_module_class.bl_icon
new_db.bl_name = type_module_class.bl_id
new_db.icon = impl.bl_icon
new_db.bl_name = impl.bl_id
def client_list_callback(scene, context):
@@ -598,52 +501,15 @@ class SessionProps(bpy.types.PropertyGroup):
description='Session password',
subtype='PASSWORD'
)
internet_ip: bpy.props.StringProperty(
name="internet ip",
default="no found",
description='Internet interface ip',
)
user_snap_running: bpy.props.BoolProperty(
default=False
)
time_snap_running: bpy.props.BoolProperty(
default=False
)
is_host: bpy.props.BoolProperty(
default=False
)
replay_files: bpy.props.CollectionProperty(
name='File paths',
type=bpy.types.OperatorFileListElement
)
replay_persistent_collection: bpy.props.BoolProperty(
name="replay_persistent_collection",
description='Enable a collection that persist accross frames loading',
get=get_replay_persistent_collection,
set=set_replay_persistent_collection,
)
replay_mode: bpy.props.EnumProperty(
name='replay method',
description='Replay in keyframe (timeline) or manually',
items={
('TIMELINE', 'TIMELINE', 'Replay from the timeline.'),
('MANUAL', 'MANUAL', 'Replay manually, from the replay frame widget.')},
default='TIMELINE')
replay_duration: bpy.props.IntProperty(
name='replay interval',
default=250,
min=10,
set=set_replay_duration,
get=get_replay_duration,
)
replay_frame_current: bpy.props.IntProperty(
name='replay_frame_current',
)
replay_camera: bpy.props.PointerProperty(
name='Replay camera',
type=bpy.types.Object
)
def get_preferences():
return bpy.context.preferences.addons[__package__].preferences
classes = (
SessionUser,
@@ -662,23 +528,12 @@ def register():
prefs = bpy.context.preferences.addons[__package__].preferences
if len(prefs.supported_datablocks) == 0:
logging.debug('Generating bl_types preferences')
logging.debug('Generating io_bpy preferences')
prefs.generate_supported_types()
bpy.types.Scene.active_replay_file = bpy.props.IntProperty(
name="active_replay_file",
default=0,
min=0,
description='Active snapshot',
set=set_active_replay,
get=get_active_replay,
options={'ANIMATABLE'}
)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
del bpy.types.Scene.active_replay_file
+2 -2
View File
@@ -30,7 +30,7 @@ import mathutils
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG,
STATE_INITIAL, STATE_LAUNCHING_SERVICES,
STATE_INITIAL, CONNECTING,
STATE_LOBBY, STATE_QUITTING, STATE_SRV_SYNC,
STATE_SYNCING, STATE_WAITING)
from replication.interface import session
@@ -399,7 +399,7 @@ class SessionStatusWidget(Widget):
text_scale = self.preferences.presence_hud_scale
ui_scale = bpy.context.preferences.view.ui_scale
color = [1, 1, 0, 1]
state = session.state.get('STATE')
state = session.state
state_str = f"{get_state_str(state)}"
if state == STATE_ACTIVE:
+24 -17
View File
@@ -24,6 +24,7 @@ from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
STATE_SRV_SYNC, STATE_SYNCING, UP)
from replication.exception import NonAuthorizedOperationError, ContextError
from replication.interface import session
from replication import porcelain
from . import operators, utils
from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget,
@@ -71,7 +72,7 @@ class Timer(object):
except Exception as e:
logging.error(e)
self.unregister()
session.disconnect()
session.disconnect(reason=f"Error during timer {self.id} execution")
else:
if self.is_running:
return self._timeout
@@ -100,25 +101,31 @@ class SessionBackupTimer(Timer):
def execute(self):
session.save(self._filepath)
class SessionListenTimer(Timer):
def execute(self):
session.listen()
class ApplyTimer(Timer):
def execute(self):
if session and session.state['STATE'] == STATE_ACTIVE:
if session and session.state == STATE_ACTIVE:
nodes = session.list()
for node in nodes:
node_ref = session.get(uuid=node)
node_ref = session.repository.get_node(node)
if node_ref.state == FETCHED:
try:
session.apply(node)
porcelain.apply(session.repository, node)
except Exception as e:
logging.error(f"Fail to apply {node_ref.uuid}")
traceback.print_exc()
else:
if node_ref.bl_reload_parent:
for parent in session._graph.find_parents(node):
for parent in session.repository.get_parents(node):
logging.debug("Refresh parent {node}")
session.apply(parent, force=True)
porcelain.apply(session.repository,
parent.uuid,
force=True)
class DynamicRightSelectTimer(Timer):
@@ -131,7 +138,7 @@ class DynamicRightSelectTimer(Timer):
def execute(self):
settings = utils.get_preferences()
if session and session.state['STATE'] == STATE_ACTIVE:
if session and session.state == STATE_ACTIVE:
# Find user
if self._user is None:
self._user = session.online_users.get(settings.username)
@@ -145,7 +152,7 @@ class DynamicRightSelectTimer(Timer):
# if an annotation exist and is tracked
if annotation_gp and annotation_gp.uuid:
registered_gp = session.get(uuid=annotation_gp.uuid)
registered_gp = session.repository.get_node(annotation_gp.uuid)
if is_annotating(bpy.context):
# try to get the right on it
if registered_gp.owner == RP_COMMON:
@@ -159,9 +166,9 @@ class DynamicRightSelectTimer(Timer):
affect_dependencies=False)
if registered_gp.owner == settings.username:
gp_node = session.get(uuid=annotation_gp.uuid)
gp_node = session.repository.get_node(annotation_gp.uuid)
if gp_node.has_changed():
session.commit(gp_node.uuid)
porcelain.commit(session.repository, gp_node.uuid)
session.push(gp_node.uuid, check_data=False)
elif self._annotating:
@@ -183,7 +190,7 @@ class DynamicRightSelectTimer(Timer):
# change old selection right to common
for obj in obj_common:
node = session.get(uuid=obj)
node = session.repository.get_node(obj)
if node and (node.owner == settings.username or node.owner == RP_COMMON):
recursive = True
@@ -201,7 +208,7 @@ class DynamicRightSelectTimer(Timer):
# change new selection to our
for obj in obj_ours:
node = session.get(uuid=obj)
node = session.repository.get_node(obj)
if node and node.owner == RP_COMMON:
recursive = True
@@ -234,7 +241,7 @@ class DynamicRightSelectTimer(Timer):
owned_keys = session.list(
filter_owner=settings.username)
for key in owned_keys:
node = session.get(uuid=key)
node = session.repository.get_node(key)
try:
session.change_owner(
key,
@@ -248,7 +255,8 @@ class DynamicRightSelectTimer(Timer):
for obj in bpy.data.objects:
object_uuid = getattr(obj, 'uuid', None)
if object_uuid:
is_selectable = not session.is_readonly(object_uuid)
node = session.repository.get_node(object_uuid)
is_selectable = not node.owner in [settings.username, RP_COMMON]
if obj.hide_select != is_selectable:
obj.hide_select = is_selectable
@@ -263,9 +271,8 @@ class ClientUpdate(Timer):
settings = utils.get_preferences()
if session and renderer:
if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]:
local_user = session.online_users.get(
settings.username)
if session.state in [STATE_ACTIVE, STATE_LOBBY]:
local_user = session.online_users.get(settings.username)
if not local_user:
return
+21 -60
View File
@@ -26,7 +26,7 @@ from replication.constants import (ADDED, ERROR, FETCHED,
STATE_INITIAL, STATE_SRV_SYNC,
STATE_WAITING, STATE_QUITTING,
STATE_LOBBY,
STATE_LAUNCHING_SERVICES)
CONNECTING)
from replication import __version__
from replication.interface import session
from .timers import registry
@@ -71,9 +71,9 @@ class SESSION_PT_settings(bpy.types.Panel):
def draw_header(self, context):
layout = self.layout
if session and session.state['STATE'] != STATE_INITIAL:
if session and session.state != STATE_INITIAL:
cli_state = session.state
state = session.state.get('STATE')
state = session.state
connection_icon = "KEYTYPE_MOVING_HOLD_VEC"
if state == STATE_ACTIVE:
@@ -81,7 +81,7 @@ class SESSION_PT_settings(bpy.types.Panel):
else:
connection_icon = 'PROP_CON'
layout.label(text=f"Session - {get_state_str(cli_state['STATE'])}", icon=connection_icon)
layout.label(text=f"Session - {get_state_str(cli_state)}", icon=connection_icon)
else:
layout.label(text=f"Session - v{__version__}",icon="PROP_OFF")
@@ -94,13 +94,13 @@ class SESSION_PT_settings(bpy.types.Panel):
if hasattr(context.window_manager, 'session'):
# STATE INITIAL
if not session \
or (session and session.state['STATE'] == STATE_INITIAL):
or (session and session.state == STATE_INITIAL):
pass
else:
cli_state = session.state
progress = session.state_progress
row = layout.row()
current_state = cli_state['STATE']
current_state = session.state
info_msg = None
if current_state in [STATE_ACTIVE]:
@@ -111,8 +111,6 @@ class SESSION_PT_settings(bpy.types.Panel):
row= layout.row()
if current_state in [STATE_ACTIVE] and runtime_settings.is_host:
info_msg = f"LAN: {runtime_settings.internet_ip}"
if current_state == STATE_LOBBY:
info_msg = "Waiting for the session to start."
@@ -124,8 +122,8 @@ class SESSION_PT_settings(bpy.types.Panel):
if current_state in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]:
info_box = row.box()
info_box.row().label(text=printProgressBar(
cli_state['CURRENT'],
cli_state['TOTAL'],
progress['current'],
progress['total'],
length=16
))
@@ -141,7 +139,7 @@ class SESSION_PT_settings_network(bpy.types.Panel):
@classmethod
def poll(cls, context):
return not session \
or (session and session.state['STATE'] == 0)
or (session and session.state == 0)
def draw_header(self, context):
self.layout.label(text="", icon='URL')
@@ -199,7 +197,7 @@ class SESSION_PT_settings_user(bpy.types.Panel):
@classmethod
def poll(cls, context):
return not session \
or (session and session.state['STATE'] == 0)
or (session and session.state == 0)
def draw_header(self, context):
self.layout.label(text="", icon='USER')
@@ -230,7 +228,7 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
@classmethod
def poll(cls, context):
return not session \
or (session and session.state['STATE'] == 0)
or (session and session.state == 0)
def draw_header(self, context):
self.layout.label(text="", icon='PREFERENCES')
@@ -251,9 +249,6 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
emboss=False)
if settings.sidebar_advanced_net_expanded:
net_section_row = net_section.row()
net_section_row.label(text="IPC Port:")
net_section_row.prop(settings, "ipc_port", text="")
net_section_row = net_section.row()
net_section_row.label(text="Timeout (ms):")
net_section_row.prop(settings, "connection_timeout", text="")
@@ -322,7 +317,7 @@ class SESSION_PT_user(bpy.types.Panel):
@classmethod
def poll(cls, context):
return session and session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]
return session and session.state in [STATE_ACTIVE, STATE_LOBBY]
def draw_header(self, context):
self.layout.label(text="", icon='USER')
@@ -353,7 +348,7 @@ class SESSION_PT_user(bpy.types.Panel):
if active_user != 0 and active_user.username != settings.username:
row = layout.row()
user_operations = row.split()
if session.state['STATE'] == STATE_ACTIVE:
if session.state == STATE_ACTIVE:
user_operations.alert = context.window_manager.session.time_snap_running
user_operations.operator(
@@ -411,7 +406,7 @@ class SESSION_PT_presence(bpy.types.Panel):
@classmethod
def poll(cls, context):
return not session \
or (session and session.state['STATE'] in [STATE_INITIAL, STATE_ACTIVE])
or (session and session.state in [STATE_INITIAL, STATE_ACTIVE])
def draw_header(self, context):
self.layout.prop(context.window_manager.session,
@@ -441,7 +436,7 @@ class SESSION_PT_presence(bpy.types.Panel):
def draw_property(context, parent, property_uuid, level=0):
settings = get_preferences()
runtime_settings = context.window_manager.session
item = session.get(uuid=property_uuid)
item = session.repository.get_node(property_uuid)
area_msg = parent.row(align=True)
@@ -519,8 +514,8 @@ class SESSION_PT_repository(bpy.types.Panel):
admin = usr['admin']
return hasattr(context.window_manager, 'session') and \
session and \
(session.state['STATE'] == STATE_ACTIVE or \
session.state['STATE'] == STATE_LOBBY and admin)
(session.state == STATE_ACTIVE or \
session.state == STATE_LOBBY and admin)
def draw_header(self, context):
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
@@ -536,7 +531,7 @@ class SESSION_PT_repository(bpy.types.Panel):
row = layout.row()
if session.state['STATE'] == STATE_ACTIVE:
if session.state == STATE_ACTIVE:
if 'SessionBackupTimer' in registry:
row.alert = True
row.operator('session.cancel_autosave', icon="CANCEL")
@@ -568,7 +563,7 @@ class SESSION_PT_repository(bpy.types.Panel):
filter_owner=settings.username) if runtime_settings.filter_owned else session.list()
client_keys = [key for key in key_to_filter
if session.get(uuid=key).str_type
if session.repository.get_node(key).str_type
in types_filter]
if client_keys:
@@ -579,7 +574,7 @@ class SESSION_PT_repository(bpy.types.Panel):
else:
row.label(text="Empty")
elif session.state['STATE'] == STATE_LOBBY and usr and usr['admin']:
elif session.state == STATE_LOBBY and usr and usr['admin']:
row.operator("session.init", icon='TOOL_SETTINGS', text="Init")
else:
row.label(text="Waiting to start")
@@ -615,39 +610,6 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
row.active = settings.presence_show_user
row.prop(settings, "presence_show_far_user")
class SESSION_PT_replay(bpy.types.Panel):
bl_idname = "MULTIUSER_REPLAY_PT_panel"
bl_label = "Replay"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.window_manager.session.replay_files
def draw_header(self, context):
self.layout.label(text="", icon='RECOVER_LAST')
def draw(self, context):
layout = self.layout
settings = context.window_manager.session
row= layout.row()
row.prop(settings,'replay_mode', toggle=True, expand=True)
row= layout.row()
if settings.replay_mode == 'MANUAL':
row.prop(bpy.context.scene, 'active_replay_file', text="Snapshot index")
else:
row.prop(settings, 'replay_duration', text="Replay Duration")
row= layout.row()
row.prop(settings, 'replay_persistent_collection', text="persistent collection", toggle=True, icon='OUTLINER_COLLECTION')
if settings.replay_persistent_collection:
row= layout.row()
row.prop(settings, 'replay_camera', text="", icon='VIEW_CAMERA')
classes = (
SESSION_UL_users,
SESSION_PT_settings,
@@ -657,7 +619,6 @@ classes = (
SESSION_PT_advanced_settings,
SESSION_PT_user,
SESSION_PT_repository,
SESSION_PT_replay,
VIEW3D_PT_overlay_session,
)
+14 -2
View File
@@ -36,7 +36,7 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH,
STATE_INITIAL, STATE_SRV_SYNC,
STATE_WAITING, STATE_QUITTING,
STATE_LOBBY,
STATE_LAUNCHING_SERVICES)
CONNECTING)
def find_from_attr(attr_name, attr_value, list):
@@ -92,7 +92,7 @@ def get_state_str(state):
state_str = 'OFFLINE'
elif state == STATE_QUITTING:
state_str = 'QUITTING'
elif state == STATE_LAUNCHING_SERVICES:
elif state == CONNECTING:
state_str = 'LAUNCHING SERVICES'
elif state == STATE_LOBBY:
state_str = 'LOBBY'
@@ -100,6 +100,18 @@ def get_state_str(state):
return state_str
def clean_scene():
for type_name in dir(bpy.data):
try:
type_collection = getattr(bpy.data, type_name)
for item in type_collection:
type_collection.remove(item)
except:
continue
# Clear sequencer
bpy.context.scene.sequence_editor_clear()
def get_selected_objects(scene, active_view_layer):
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_action import BlAction
from multi_user.io_bpy.bl_action import BlAction
INTERPOLATION = ['CONSTANT', 'LINEAR', 'BEZIER', 'SINE', 'QUAD', 'CUBIC', 'QUART', 'QUINT', 'EXPO', 'CIRC', 'BACK', 'BOUNCE', 'ELASTIC']
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_armature import BlArmature
from multi_user.io_bpy.bl_armature import BlArmature
def test_armature(clear_blend):
bpy.ops.object.armature_add()
+1 -1
View File
@@ -4,7 +4,7 @@ import pytest
from deepdiff import DeepDiff
import bpy
from multi_user.bl_types.bl_camera import BlCamera
from multi_user.io_bpy.bl_camera import BlCamera
@pytest.mark.parametrize('camera_type', ['PANO','PERSP','ORTHO'])
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
from uuid import uuid4
import bpy
import random
from multi_user.bl_types.bl_collection import BlCollection
from multi_user.io_bpy.bl_collection import BlCollection
def test_collection(clear_blend):
# Generate a collection with childrens and a cube
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_curve import BlCurve
from multi_user.io_bpy.bl_curve import BlCurve
@pytest.mark.parametrize('curve_type', ['TEXT','BEZIER'])
def test_curve(clear_blend, curve_type):
+1 -1
View File
@@ -4,7 +4,7 @@ import pytest
from deepdiff import DeepDiff
import bpy
from multi_user.bl_types.bl_gpencil import BlGpencil
from multi_user.io_bpy.bl_gpencil import BlGpencil
def test_gpencil(clear_blend):
+1 -1
View File
@@ -4,7 +4,7 @@ import pytest
from deepdiff import DeepDiff
import bpy
from multi_user.bl_types.bl_lattice import BlLattice
from multi_user.io_bpy.bl_lattice import BlLattice
def test_lattice(clear_blend):
+1 -1
View File
@@ -4,7 +4,7 @@ import pytest
from deepdiff import DeepDiff
import bpy
from multi_user.bl_types.bl_lightprobe import BlLightprobe
from multi_user.io_bpy.bl_lightprobe import BlLightprobe
@pytest.mark.skipif(bpy.app.version[1] < 83, reason="requires blender 2.83 or higher")
+1 -1
View File
@@ -4,7 +4,7 @@ import pytest
from deepdiff import DeepDiff
import bpy
from multi_user.bl_types.bl_light import BlLight
from multi_user.io_bpy.bl_light import BlLight
@pytest.mark.parametrize('light_type', ['SPOT','SUN','POINT','AREA'])
+1 -1
View File
@@ -4,7 +4,7 @@ import pytest
from deepdiff import DeepDiff
import bpy
from multi_user.bl_types.bl_material import BlMaterial
from multi_user.io_bpy.bl_material import BlMaterial
def test_material_nodes(clear_blend):
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_mesh import BlMesh
from multi_user.io_bpy.bl_mesh import BlMesh
@pytest.mark.parametrize('mesh_type', ['EMPTY','FILLED'])
def test_mesh(clear_blend, mesh_type):
+1 -1
View File
@@ -4,7 +4,7 @@ import pytest
from deepdiff import DeepDiff
import bpy
from multi_user.bl_types.bl_metaball import BlMetaball
from multi_user.io_bpy.bl_metaball import BlMetaball
@pytest.mark.parametrize('metaballs_type', ['PLANE','CAPSULE','BALL','ELLIPSOID','CUBE'])
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_object import BlObject
from multi_user.io_bpy.bl_object import BlObject
# Removed 'BUILD', 'SOFT_BODY' modifier because the seed doesn't seems to be
# correctly initialized (#TODO: report the bug)
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_scene import BlScene
from multi_user.io_bpy.bl_scene import BlScene
from multi_user.utils import get_preferences
def test_scene(clear_blend):
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_speaker import BlSpeaker
from multi_user.io_bpy.bl_speaker import BlSpeaker
def test_speaker(clear_blend):
bpy.ops.object.speaker_add()
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_texture import BlTexture
from multi_user.io_bpy.bl_texture import BlTexture
TEXTURE_TYPES = ['NONE', 'BLEND', 'CLOUDS', 'DISTORTED_NOISE', 'IMAGE', 'MAGIC', 'MARBLE', 'MUSGRAVE', 'NOISE', 'STUCCI', 'VORONOI', 'WOOD']
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_volume import BlVolume
from multi_user.io_bpy.bl_volume import BlVolume
def test_volume(clear_blend):
datablock = bpy.data.volumes.new("Test")
+1 -1
View File
@@ -5,7 +5,7 @@ from deepdiff import DeepDiff
import bpy
import random
from multi_user.bl_types.bl_world import BlWorld
from multi_user.io_bpy.bl_world import BlWorld
def test_world(clear_blend):
datablock = bpy.data.worlds.new('test')