Compare commits

...

74 Commits

Author SHA1 Message Date
93d50ac56b refactor: porcail api 2021-04-22 10:04:38 +02:00
0e73af4d49 refactor: update procelain import 2021-04-21 21:30:43 +02:00
00d5e622af feat: update replication 2021-04-21 14:33:18 +02:00
b358d3b305 refactor: change push api 2021-04-21 14:32:56 +02:00
203c10ccd3 feat: missing class attribute for registration 2021-04-21 11:22:53 +02:00
da349dd4a9 refactor: io_bpy architecture revamp 2021-04-21 11:10:24 +02:00
845bb11b8e fix: bl_object 2021-04-20 09:53:59 +02:00
9312d6a8c5 Merge branch 'develop' into 173-differential-revision-milestone-2-replication-refactoring 2021-04-20 09:47:23 +02:00
e538752fbc Merge branch 'master' of gitlab.com:slumber/multi-user into develop 2021-04-15 15:31:59 +02:00
53eaaa2fcd fix: auto-updater operator registration for blender 2.93 compatibility 2021-04-15 15:28:59 +02:00
a7e9108bff Merge branch 'develop' into 'master'
v0.3.0

See merge request slumber/multi-user!106
2021-04-14 14:32:24 +00:00
570909a7c4 fix: prevent field from being dumped if unused
fix: bl_object tests
2021-04-14 16:25:21 +02:00
736c3df7c4 feat: remove new particle systems
clean: remove logs
2021-04-14 15:50:53 +02:00
8e606068f3 fix: particle system duplication
feat: update Readme
2021-04-14 15:29:02 +02:00
eb631e2d4b feat: update changelog 0.3.0 release 2021-04-14 14:36:06 +02:00
70641435cc feat: initial rigid body supports 2021-04-14 12:25:16 +02:00
552c649d34 feat: physics forcefield and collision support 2021-04-14 11:49:34 +02:00
d9d5a34653 clean: remove libs 2021-04-14 09:56:07 +02:00
12acd22660 feat: ignore some attributes 2021-04-14 09:54:34 +02:00
826a59085e feat: particle texture slot support 2021-04-14 09:45:18 +02:00
5ee4988aca Merge branch '24-particle-support' into develop 2021-04-13 22:45:27 +02:00
cb85a1db4c feat: dual identification for object parents 2021-04-13 14:37:43 +02:00
ca78a42076 feat: update submodules 2021-04-06 14:23:10 +02:00
ee886e00c8 feat: generate subtree 2021-04-06 14:21:26 +02:00
5e30e215ab fix: empty node 2021-04-02 16:37:47 +02:00
9f167256d0 fix: node frame trasform 2021-04-02 16:12:51 +02:00
4e19c169b2 fix: node_groups unordered socket loading
fix: geometry_node sample texture handling
fix: geometry node dependencies
2021-04-02 15:51:31 +02:00
9c633c35ec fix: geometry node socket for blender 2.93 2021-04-02 10:01:45 +02:00
9610b50a49 Merge branch '181-geometry-nodes-int-float-inputs-doesn-t-sync' into 'develop'
Resolve "Geometry nodes int/float inputs doesn't sync"

See merge request slumber/multi-user!116
2021-03-31 13:42:26 +00:00
67d18f08e2 fix: Timer not unregistered error
fix: handle correctly unsupported float parameter for geometry nodes
fix: Material loading
2021-03-31 15:38:35 +02:00
9d0d684589 fix: geometry nodes str, float, int loading 2021-03-31 11:19:03 +02:00
9c7043e84c fix: animation data error 2021-03-26 16:14:27 +01:00
e659b7da94 refactor: move implementation to static def 2021-03-26 12:30:15 +01:00
e3af69a9c8 feat: add replication to the submodules 2021-03-25 14:55:53 +01:00
328c651cea remove data handlign from ReplicatedDatablock 2021-03-24 16:08:12 +01:00
d4224c789a refactor: move commit to porcelain
feractor: remove is_ readonly
2021-03-24 11:20:40 +01:00
2446df4fe3 feat: raise the default timeout to 5 second 2021-03-21 09:28:54 +01:00
07862f1cf0 fix: missing hue_interpolation 2021-03-19 11:07:04 +01:00
3a02711baa feat: faster root management 2021-03-14 20:58:25 +01:00
c7e8002fed fix: apply api
clean: ipc port propertie
2021-03-14 18:32:04 +01:00
f4e7ec6be8 Merge branch 'develop' into 173-differential-revision-milestone-2-replication-refactoring 2021-03-14 17:46:23 +01:00
480818fe85 Merge branch '180-parent-relation-have-doesn-t-keeps-transform' into 'develop'
Resolve "Parenting objects doesn't keeps transform"

See merge request slumber/multi-user!115
2021-03-13 17:35:42 +00:00
b965c80ba5 fix: parent transform
fix: race  condition for COMMON objects

related to #180
2021-03-13 18:32:20 +01:00
235db712fd fix: api 2021-03-11 15:45:48 +01:00
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
8e3c86561f refactor: move add to porcelain 2021-03-09 10:19:51 +01:00
dba19e831d Merge branch 'develop' into 173-differential-revision-milestone-2-replication-refactoring 2021-03-08 22:16:14 +01:00
93df5ca5fa fix: disconnect callback 2021-03-06 10:20:57 +01:00
b17104c67e fix: naming 2021-03-05 10:35:35 +01:00
b66d0dd4ce Merge branch 'develop' of gitlab.com:slumber/multi-user into develop 2021-03-04 15:49:00 +01:00
9487753307 feat: fix object and collection support for geometry nodes 2021-03-04 15:48:36 +01:00
df1257ca4c Merge branch '179-parent-relation-can-t-be-removed' into 'develop'
Resolve "Parent relation can't be removed"

See merge request slumber/multi-user!113
2021-03-04 13:25:39 +00:00
875b9ce934 feat: temporary disable CI jobs for this branch because of breaking changes 2021-03-04 14:24:03 +01:00
2d638ef76f refactor: interface api changes 2021-03-04 14:22:54 +01:00
cc5a87adb8 fix: prevent matrix_parent_inverse from being reset by loading parents only if its necessary 2021-03-03 11:00:47 +01:00
19c56e590b feat: remove parent as node dependency 2021-03-03 10:03:57 +01:00
d0e80da945 fix: object parenting can't be removed
Related to #179
2021-03-03 09:55:48 +01:00
0ccd0563ea feat: testing doc building with python 3.8 2021-03-02 12:56:12 +00:00
1c3394ce56 feat: sphinx-material theme 2021-03-02 12:46:26 +00:00
d2b63df68e Merge branch '178-move-documentation-hosting-to-gitlab-page' into 'develop'
Resolve "Move documentation hosting to gitlab page"

See merge request slumber/multi-user!112
2021-03-02 09:21:57 +00:00
3d9c78c2f9 doc: only build for master/develop 2021-03-02 10:18:11 +01:00
4726a90a4a doc: reflect doc hosting changes to the Readme.md 2021-03-02 10:16:59 +01:00
73b763d85f fix: job ordering error 2021-03-02 09:09:50 +00:00
5e29c6fe26 Update .gitlab/ci/doc.gitlab-ci.yml 2021-03-02 09:08:39 +00:00
113ab81cbf Update .gitlab/ci/doc.gitlab-ci.yml 2021-03-02 09:07:50 +00:00
d2215b662c feat: update jobs dependencies 2021-03-02 09:06:32 +00:00
238a34d023 feat: needs test to success 2021-03-02 09:05:21 +00:00
55ca8a7b84 Update .gitlab/ci/doc.gitlab-ci.yml 2021-03-02 09:03:19 +00:00
7049c1723d feat: initial CI job for building the documentation for gitlab page 2021-03-02 09:58:06 +01:00
ffe419a46e Merge branch 'develop' into 'master'
v0.2.0

See merge request slumber/multi-user!73
2020-12-17 13:34:41 +00:00
bed33ca6ba Merge branch 'develop' into 'master'
v0.1.1

See merge request slumber/multi-user!54
2020-10-16 09:11:20 +00:00
56ea93508c Merge branch 'develop' into 24-particle-support 2020-04-03 18:23:29 +02:00
5f95eadc1d feat: test particle cache access 2020-03-11 18:37:43 +01:00
40ad96b0af feat: initial particle system support
Related to #24
2020-03-11 17:45:56 +01:00
66 changed files with 1394 additions and 1134 deletions

View File

@ -1,10 +0,0 @@
stages:
- test
- build
- deploy
include:
- local: .gitlab/ci/test.gitlab-ci.yml
- local: .gitlab/ci/build.gitlab-ci.yml
- local: .gitlab/ci/deploy.gitlab-ci.yml

View File

@ -1,5 +1,6 @@
build:
stage: build
needs: ["test"]
image: debian:stable-slim
script:
- rm -rf tests .git .gitignore script

View File

@ -1,5 +1,6 @@
deploy:
stage: deploy
needs: ["build"]
image: slumber/docker-python
variables:
DOCKER_DRIVER: overlay2

View File

@ -0,0 +1,16 @@
pages:
stage: doc
needs: ["deploy"]
image: python
script:
- pip install -U sphinx sphinx_rtd_theme sphinx-material
- sphinx-build -b html ./docs public
artifacts:
paths:
- public
only:
refs:
- master
- develop

3
.gitmodules vendored
View File

@ -0,0 +1,3 @@
[submodule "multi_user/libs/replication"]
path = multi_user/libs/replication
url = https://gitlab.com/slumber/replication

View File

@ -157,4 +157,33 @@ All notable changes to this project will be documented in this file.
- Empty and Light object selection highlights
- Material renaming
- Default material nodes input parameters
- blender 2.91 python api compatibility
- blender 2.91 python api compatibility
## [0.3.0] - 2021-04-14
### Added
- Curve material support
- Cycle visibility settings
- Session save/load operator
- Add new scene support
- Physic initial support
- Geometry node initial support
- Blender 2.93 compatibility
### Changed
- Host documentation on Gitlab Page
- Event driven update (from the blender deps graph)
### Fixed
- Vertex group assignation
- Parent relation can't be removed
- Separate object
- Delete animation
- Sync missing holdout option for grease pencil material
- Sync missing `skin_vertices`
- Exception access violation during Undo/Redo
- Sync missing armature bone Roll
- Sync missing driver data_path
- Constraint replication

View File

@ -19,44 +19,46 @@ This tool aims to allow multiple users to work on the same scene over the networ
## Usage
See the [documentation](https://multi-user.readthedocs.io/en/latest/) for details.
See the [documentation](https://slumber.gitlab.io/multi-user/index.html) for details.
## Troubleshooting
See the [troubleshooting guide](https://multi-user.readthedocs.io/en/latest/getting_started/troubleshooting.html) for tips on the most common issues.
See the [troubleshooting guide](https://slumber.gitlab.io/multi-user/getting_started/troubleshooting.html) for tips on the most common issues.
## Current development status
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
| Name | Status | Comment |
| ----------- | :----: | :--------------------------------------------------------------------------: |
| action | ✔️ | |
| armature | ❗ | Not stable |
| camera | ✔️ | |
| collection | ✔️ | |
| curve | ❗ | Nurbs not supported |
| gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) |
| image | ✔️ | |
| mesh | ✔️ | |
| material | ✔️ | |
| node_groups | ❗ | Material only |
| metaball | ✔️ | |
| object | ✔️ | |
| textures | | Supported for modifiers only |
| texts | ✔️ | |
| scene | ✔️ | |
| world | ✔️ | |
| lightprobes | ✔️ | |
| compositing | | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
| nla | ❌ | |
| volumes | ✔️ | |
| particles | | [On-going](https://gitlab.com/slumber/multi-user/-/issues/24) |
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
| vse | ❗ | Mask and Clip not supported yet |
| physics | | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
| libraries | | Partial |
| Name | Status | Comment |
| -------------- | :----: | :----------------------------------------------------------: |
| action | ✔️ | |
| armature | ❗ | Not stable |
| camera | ✔️ | |
| collection | ✔️ | |
| curve | ❗ | Nurbs surfaces not supported |
| gpencil | ✔️ | |
| image | ✔️ | |
| mesh | ✔️ | |
| material | ✔️ | |
| node_groups | ❗ | Material & Geometry only |
| geometry nodes | ✔️ | |
| metaball | ✔️ | |
| object | ✔️ | |
| textures | | Supported for modifiers/materials/geo nodes only |
| texts | ✔️ | |
| scene | ✔️ | |
| world | ✔️ | |
| lightprobes | ✔️ | |
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
| nla | ❌ | |
| volumes | ✔️ | |
| particles | ❗ | The cache isn't syncing. |
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
| vse | | Mask and Clip not supported yet |
| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
| libraries | ❗ | Partial |
### Performance issues
@ -74,7 +76,7 @@ I'm working on it.
## Contributing
See [contributing section](https://multi-user.readthedocs.io/en/latest/ways_to_contribute.html) of the documentation.
See [contributing section](https://slumber.gitlab.io/multi-user/ways_to_contribute.html) of the documentation.
Feel free to [join the discord server](https://discord.gg/aBPvGws) to chat, seek help and contribute.

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.

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)

View File

@ -122,13 +122,13 @@ class addon_updater_install_popup(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains
clean_install = bpy.props.BoolProperty(
clean_install: bpy.props.BoolProperty(
name="Clean install",
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
default=False,
options={'HIDDEN'}
)
ignore_enum = bpy.props.EnumProperty(
ignore_enum: bpy.props.EnumProperty(
name="Process update",
description="Decide to install, ignore, or defer new addon update",
items=[
@ -264,7 +264,7 @@ class addon_updater_update_now(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains
clean_install = bpy.props.BoolProperty(
clean_install: bpy.props.BoolProperty(
name="Clean install",
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
default=False,
@ -332,7 +332,7 @@ class addon_updater_update_target(bpy.types.Operator):
i+=1
return ret
target = bpy.props.EnumProperty(
target: bpy.props.EnumProperty(
name="Target version to install",
description="Select the version to install",
items=target_version
@ -341,7 +341,7 @@ class addon_updater_update_target(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains
clean_install = bpy.props.BoolProperty(
clean_install: bpy.props.BoolProperty(
name="Clean install",
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
default=False,
@ -399,7 +399,7 @@ class addon_updater_install_manually(bpy.types.Operator):
bl_description = "Proceed to manually install update"
bl_options = {'REGISTER', 'INTERNAL'}
error = bpy.props.StringProperty(
error: bpy.props.StringProperty(
name="Error Occurred",
default="",
options={'HIDDEN'}
@ -461,7 +461,7 @@ class addon_updater_updated_successful(bpy.types.Operator):
bl_description = "Update installation response"
bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
error = bpy.props.StringProperty(
error: bpy.props.StringProperty(
name="Error Occurred",
default="",
options={'HIDDEN'}

View File

@ -1,55 +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',
] # 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__

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'])

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

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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)

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())

View File

@ -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
"""

View 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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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")

View File

@ -24,17 +24,19 @@ 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']
IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM']
def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
""" Load a node into a node_tree from a dict
: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()
@ -53,31 +55,136 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
inputs_data = node_data.get('inputs')
if inputs_data:
inputs = [i for i in target_node.inputs if i.type not in IGNORED_SOCKETS]
for idx, inpt in enumerate(inputs_data):
if idx < len(inputs) and hasattr(inputs[idx], "default_value"):
for idx, inpt in enumerate(inputs):
if idx < len(inputs_data) and hasattr(inpt, "default_value"):
loaded_input = inputs_data[idx]
try:
inputs[idx].default_value = inpt
if inpt.type in ['OBJECT', 'COLLECTION']:
inpt.default_value = get_datablock_from_uuid(loaded_input, None)
else:
inpt.default_value = loaded_input
except Exception as e:
logging.warning(f"Node {target_node.name} input {inputs[idx].name} parameter not supported, skipping ({e})")
logging.warning(f"Node {target_node.name} input {inpt.name} parameter not supported, skipping ({e})")
else:
logging.warning(f"Node {target_node.name} input length mismatch.")
outputs_data = node_data.get('outputs')
if outputs_data:
outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS]
for idx, output in enumerate(outputs_data):
if idx < len(outputs) and hasattr(outputs[idx], "default_value"):
for idx, output in enumerate(outputs):
if idx < len(outputs_data) and hasattr(output, "default_value"):
loaded_output = outputs_data[idx]
try:
outputs[idx].default_value = output
if output.type in ['OBJECT', 'COLLECTION']:
output.default_value = get_datablock_from_uuid(loaded_output, None)
else:
output.default_value = loaded_output
except Exception as e:
logging.warning(
f"Node {target_node.name} output {outputs[idx].name} parameter not supported, skipping ({e})")
f"Node {target_node.name} output {output.name} parameter not supported, skipping ({e})")
else:
logging.warning(
f"Node {target_node.name} output length mismatch.")
def dump_node(node: bpy.types.ShaderNode) -> dict:
""" Dump a single node to a dict
:arg node: datablock node
:type node: bpy.types.Node
:retrun: dict
"""
node_dumper = Dumper()
node_dumper.depth = 1
node_dumper.exclude_filter = [
"dimensions",
"show_expanded",
"name_full",
"select",
"bl_label",
"bl_height_min",
"bl_height_max",
"bl_height_default",
"bl_width_min",
"bl_width_max",
"type",
"bl_icon",
"bl_width_default",
"bl_static_type",
"show_tetxure",
"is_active_output",
"hide",
"show_options",
"show_preview",
"show_texture",
"outputs",
"width_hidden",
"image"
]
dumped_node = node_dumper.dump(node)
if node.parent:
dumped_node['parent'] = node.parent.name
dump_io_needed = (node.type not in ['REROUTE', 'OUTPUT_MATERIAL'])
if dump_io_needed:
io_dumper = Dumper()
io_dumper.depth = 2
io_dumper.include_filter = ["default_value"]
if hasattr(node, 'inputs'):
dumped_node['inputs'] = []
inputs = [i for i in node.inputs if i.type not in IGNORED_SOCKETS]
for idx, inpt in enumerate(inputs):
if hasattr(inpt, 'default_value'):
if isinstance(inpt.default_value, bpy.types.ID):
dumped_input = inpt.default_value.uuid
else:
dumped_input = io_dumper.dump(inpt.default_value)
dumped_node['inputs'].append(dumped_input)
if hasattr(node, 'outputs'):
dumped_node['outputs'] = []
for idx, output in enumerate(node.outputs):
if output.type not in IGNORED_SOCKETS:
if hasattr(output, 'default_value'):
dumped_node['outputs'].append(
io_dumper.dump(output.default_value))
if hasattr(node, 'color_ramp'):
ramp_dumper = Dumper()
ramp_dumper.depth = 4
ramp_dumper.include_filter = [
'elements',
'alpha',
'color',
'position',
'interpolation',
'hue_interpolation',
'color_mode'
]
dumped_node['color_ramp'] = ramp_dumper.dump(node.color_ramp)
if hasattr(node, 'mapping'):
curve_dumper = Dumper()
curve_dumper.depth = 5
curve_dumper.include_filter = [
'curves',
'points',
'location'
]
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
if hasattr(node, 'image') and getattr(node, 'image'):
dumped_node['image_uuid'] = node.image.uuid
if hasattr(node, 'node_tree') and getattr(node, 'node_tree'):
dumped_node['node_tree_uuid'] = node.node_tree.uuid
return dumped_node
def load_links(links_data, node_tree):
""" Load node_tree links from a list
@ -120,93 +227,6 @@ def dump_links(links):
return links_data
def dump_node(node: bpy.types.ShaderNode) -> dict:
""" Dump a single node to a dict
:arg node: target node
:type node: bpy.types.Node
:retrun: dict
"""
node_dumper = Dumper()
node_dumper.depth = 1
node_dumper.exclude_filter = [
"dimensions",
"show_expanded",
"name_full",
"select",
"bl_label",
"bl_height_min",
"bl_height_max",
"bl_height_default",
"bl_width_min",
"bl_width_max",
"type",
"bl_icon",
"bl_width_default",
"bl_static_type",
"show_tetxure",
"is_active_output",
"hide",
"show_options",
"show_preview",
"show_texture",
"outputs",
"width_hidden",
"image"
]
dumped_node = node_dumper.dump(node)
dump_io_needed = (node.type not in ['REROUTE', 'OUTPUT_MATERIAL'])
if dump_io_needed:
io_dumper = Dumper()
io_dumper.depth = 2
io_dumper.include_filter = ["default_value"]
if hasattr(node, 'inputs'):
dumped_node['inputs'] = []
for idx, inpt in enumerate(node.inputs):
if hasattr(inpt, 'default_value'):
dumped_node['inputs'].append(
io_dumper.dump(inpt.default_value))
if hasattr(node, 'outputs'):
dumped_node['outputs'] = []
for idx, output in enumerate(node.outputs):
if hasattr(output, 'default_value'):
dumped_node['outputs'].append(
io_dumper.dump(output.default_value))
if hasattr(node, 'color_ramp'):
ramp_dumper = Dumper()
ramp_dumper.depth = 4
ramp_dumper.include_filter = [
'elements',
'alpha',
'color',
'position',
'interpolation',
'color_mode'
]
dumped_node['color_ramp'] = ramp_dumper.dump(node.color_ramp)
if hasattr(node, 'mapping'):
curve_dumper = Dumper()
curve_dumper.depth = 5
curve_dumper.include_filter = [
'curves',
'points',
'location'
]
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
if hasattr(node, 'image') and getattr(node, 'image'):
dumped_node['image_uuid'] = node.image.uuid
if hasattr(node, 'node_tree') and getattr(node, 'node_tree'):
dumped_node['node_tree_uuid'] = node.node_tree.uuid
return dumped_node
def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
""" Dump a shader node_tree to a dict including links and nodes
@ -231,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
@ -254,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
@ -263,7 +283,7 @@ def load_node_tree_sockets(sockets: bpy.types.Collection,
"""
# Check for removed sockets
for socket in sockets:
if not [s for s in sockets_data if socket['uuid'] == s[2]]:
if not [s for s in sockets_data if 'uuid' in socket and socket['uuid'] == s[2]]:
sockets.remove(socket)
# Check for new sockets
@ -282,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
@ -303,6 +323,14 @@ def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeT
for node in node_tree_data["nodes"]:
load_node(node_tree_data["nodes"][node], target_node_tree)
for node_id, node_data in node_tree_data["nodes"].items():
target_node = target_node_tree.nodes.get(node_id, None)
if target_node is None:
continue
elif 'parent' in node_data:
target_node.parent = target_node_tree.nodes[node_data['parent']]
else:
target_node.parent = None
# TODO: load only required nodes links
# Load nodes links
target_node_tree.links.clear()
@ -317,6 +345,8 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
def has_node_group(node): return (
hasattr(node, 'node_tree') and node.node_tree)
def has_texture(node): return (
node.type in ['ATTRIBUTE_SAMPLE_TEXTURE','TEXTURE'] and node.texture)
deps = []
for node in node_tree.nodes:
@ -324,6 +354,8 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
deps.append(node.image)
elif has_node_group(node):
deps.append(node.node_tree)
elif has_texture(node):
deps.append(node.texture)
return deps
@ -343,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
@ -354,44 +386,46 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
if mat_uuid is not None:
mat_ref = get_datablock_from_uuid(mat_uuid, None)
else:
mat_ref = bpy.data.materials.get(mat_name, None)
if mat_ref is None:
raise Exception(f"Material {mat_name} doesn't exist")
mat_ref = bpy.data.materials[mat_name]
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 = [
@ -417,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
@ -453,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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -21,8 +21,17 @@ 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,
Loader,
@ -30,32 +39,97 @@ from .dump_anything import (
np_dump_collection)
SKIN_DATA = [
'radius',
'use_loose',
'use_root'
]
def get_input_index(e):
return int(re.findall('[0-9]+', e)[0])
if bpy.app.version[1] >= 93:
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float)
else:
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str)
logging.warning("Geometry node Float parameter not supported in \
blender 2.92.")
def get_node_group_inputs(node_group):
inputs = []
for inpt in node_group.inputs:
if inpt.type in IGNORED_SOCKETS:
continue
else:
inputs.append(inpt)
return inputs
# return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS]
def dump_physics(target: bpy.types.Object)->dict:
"""
Dump all physics settings from a given object excluding modifier
related physics settings (such as softbody, cloth, dynapaint and fluid)
"""
dumper = Dumper()
dumper.depth = 1
physics_data = {}
# Collisions (collision)
if target.collision and target.collision.use:
physics_data['collision'] = dumper.dump(target.collision)
# Field (field)
if target.field and target.field.type != "NONE":
physics_data['field'] = dumper.dump(target.field)
# Rigid Body (rigid_body)
if target.rigid_body:
physics_data['rigid_body'] = dumper.dump(target.rigid_body)
# Rigid Body constraint (rigid_body_constraint)
if target.rigid_body_constraint:
physics_data['rigid_body_constraint'] = dumper.dump(target.rigid_body_constraint)
return physics_data
def load_physics(dumped_settings: dict, target: bpy.types.Object):
""" Load all physics settings from a given object excluding modifier
related physics settings (such as softbody, cloth, dynapaint and fluid)
"""
loader = Loader()
if 'collision' in dumped_settings:
loader.load(target.collision, dumped_settings['collision'])
if 'field' in dumped_settings:
loader.load(target.field, dumped_settings['field'])
if 'rigid_body' in dumped_settings:
if not target.rigid_body:
bpy.ops.rigidbody.object_add({"object": target})
loader.load(target.rigid_body, dumped_settings['rigid_body'])
elif target.rigid_body:
bpy.ops.rigidbody.object_remove({"object": target})
if 'rigid_body_constraint' in dumped_settings:
if not target.rigid_body_constraint:
bpy.ops.rigidbody.constraint_add({"object": target})
loader.load(target.rigid_body_constraint, dumped_settings['rigid_body_constraint'])
elif target.rigid_body_constraint:
bpy.ops.rigidbody.constraint_remove({"object": target})
def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
""" Dump geometry node modifier input properties
:arg modifier: geometry node modifier to dump
:type modifier: bpy.type.Modifier
"""
inputs_name = [p for p in dir(modifier) if "Input_" in p]
inputs_name.sort(key=get_input_index)
dumped_inputs = []
for inputs_index, input_name in enumerate(inputs_name):
input_value = modifier[input_name]
for inpt in get_node_group_inputs(modifier.node_group):
input_value = modifier[inpt.identifier]
dumped_input = None
if isinstance(input_value, bpy.types.ID):
dumped_input = input_value.uuid
elif type(input_value) in [int, str, float]:
elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
dumped_input = input_value
elif hasattr(input_value, 'to_list'):
dumped_input = input_value.to_list()
@ -73,18 +147,16 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b
:type target_modifier: bpy.type.Modifier
"""
inputs_name = [p for p in dir(target_modifier) if "Input_" in p]
inputs_name.sort(key=get_input_index)
for input_index, input_name in enumerate(inputs_name):
for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)):
dumped_value = dumped_modifier['inputs'][input_index]
input_value = target_modifier[input_name]
if type(input_value) in [int, str, float]:
input_value = dumped_value
input_value = target_modifier[inpt.identifier]
if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
target_modifier[inpt.identifier] = dumped_value
elif hasattr(input_value, 'to_list'):
for index in range(len(input_value)):
input_value[index] = dumped_value[index]
else:
target_modifier[input_name] = get_datablock_from_uuid(
elif inpt.type in ['COLLECTION', 'OBJECT']:
target_modifier[inpt.identifier] = get_datablock_from_uuid(
dumped_value, None)
@ -95,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:
@ -161,19 +228,25 @@ def find_textures_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy
return textures
def find_geometry_nodes(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]:
""" Find geometry nodes group from a modifier stack
def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]:
""" Find geometry nodes dependencies from a modifier stack
:arg modifiers: modifiers collection
:type modifiers: bpy.types.bpy_prop_collection
:return: list of bpy.types.NodeTree pointers
"""
nodes_groups = []
for item in modifiers:
if item.type == 'NODES' and item.node_group:
nodes_groups.append(item.node_group)
dependencies = []
for mod in modifiers:
if mod.type == 'NODES' and mod.node_group:
dependencies.append(mod.node_group)
# for inpt in get_node_group_inputs(mod.node_group):
# parameter = mod.get(inpt.identifier)
# if parameter and isinstance(parameter, bpy.types.ID):
# dependencies.append(parameter)
return dependencies
return nodes_groups
def dump_vertex_groups(src_object: bpy.types.Object) -> dict:
""" Dump object's vertex groups
@ -219,119 +292,128 @@ def load_vertex_groups(dumped_vertex_groups: dict, target_object: bpy.types.Obje
for index, weight in vg['vertices']:
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 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\
@ -342,31 +424,66 @@ class BlObject(BlDatablock):
skin_data.data,
SKIN_DATA)
if hasattr(target, 'cycles_visibility') \
and 'cycles_visibility' in data:
loader.load(target.cycles_visibility, data['cycles_visibility'])
if hasattr(datablock, 'cycles_visibility') \
and 'cycles_visibility' in data:
loader.load(datablock.cycles_visibility, data['cycles_visibility'])
# TODO: handle geometry nodes input from dump_anything
if hasattr(target, 'modifiers'):
nodes_modifiers = [mod for mod in target.modifiers if mod.type == 'NODES']
if hasattr(datablock, 'modifiers'):
nodes_modifiers = [
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)
load_modifier_geometry_node_inputs(
data['modifiers'][modifier.name], modifier)
def _dump_implementation(self, data, instance=None):
assert(instance)
particles_modifiers = [
mod for mod in datablock.modifiers if mod.type == 'PARTICLE_SYSTEM']
if _is_editmode(instance):
for mod in particles_modifiers:
default = mod.particle_system.settings
dumped_particles = data['modifiers'][mod.name]['particle_system']
loader.load(mod.particle_system, dumped_particles)
settings = get_datablock_from_uuid(dumped_particles['settings_uuid'], None)
if settings:
mod.particle_system.settings = settings
# Hack to remove the default generated particle settings
if not default.uuid:
bpy.data.particles.remove(default)
phys_modifiers = [
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, datablock)
transform = data.get('transforms', None)
if transform:
datablock.matrix_parent_inverse = mathutils.Matrix(
transform['matrix_parent_inverse'])
datablock.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
datablock.matrix_local = mathutils.Matrix(transform['matrix_local'])
@staticmethod
def dump(datablock: object) -> dict:
assert(datablock)
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",
"parent",
"data",
"library",
"empty_display_type",
@ -381,8 +498,6 @@ class BlObject(BlDatablock):
"color",
"instance_collection",
"instance_type",
"location",
"scale",
'lock_location',
'lock_rotation',
'lock_scale',
@ -396,35 +511,58 @@ class BlObject(BlDatablock):
'show_all_edges',
'show_texture_space',
'show_in_front',
'type',
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
'type'
]
data = dumper.dump(instance)
data = dumper.dump(datablock)
dumper.include_filter = [
'matrix_parent_inverse',
'matrix_local',
'matrix_basis']
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 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
dumper.exclude_filter = ['is_active']
for index, modifier in enumerate(modifiers):
data["modifiers"][modifier.name] = dumper.dump(modifier)
dumped_modifier = dumper.dump(modifier)
# hack to dump geometry nodes inputs
if modifier.type == 'NODES':
dumped_inputs = dump_modifier_geometry_node_inputs(modifier)
data["modifiers"][modifier.name]['inputs'] = dumped_inputs
gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None)
dumped_inputs = dump_modifier_geometry_node_inputs(
modifier)
dumped_modifier['inputs'] = dumped_inputs
elif modifier.type == 'PARTICLE_SYSTEM':
dumper.exclude_filter = [
"is_edited",
"is_editable",
"is_global_hair"
]
dumped_modifier['particle_system'] = dumper.dump(modifier.particle_system)
dumped_modifier['particle_system']['settings_uuid'] = modifier.particle_system.settings.uuid
elif modifier.type in ['SOFT_BODY', 'CLOTH']:
dumped_modifier['settings'] = dumper.dump(modifier.settings)
data["modifiers"][modifier.name] = dumped_modifier
gp_modifiers = getattr(datablock, 'grease_pencil_modifiers', None)
if gp_modifiers:
dumper.include_filter = None
@ -445,17 +583,18 @@ class BlObject(BlDatablock):
'location']
gp_mod_data['curve'] = curve_dumper.dump(modifier.curve)
# 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'
@ -480,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',
@ -489,13 +628,12 @@ class BlObject(BlDatablock):
bone_groups[group.name] = dumper.dump(group)
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
@ -526,11 +664,12 @@ class BlObject(BlDatablock):
if hasattr(object_data, 'skin_vertices') and object_data.skin_vertices:
skin_vertices = list()
for skin_data in object_data.skin_vertices:
skin_vertices.append(np_dump_collection(skin_data.data, SKIN_DATA))
skin_vertices.append(
np_dump_collection(skin_data.data, SKIN_DATA))
data['skin_vertices'] = skin_vertices
# CYCLE SETTINGS
if hasattr(instance, 'cycles_visibility'):
if hasattr(datablock, 'cycles_visibility'):
dumper.include_filter = [
'camera',
'diffuse',
@ -539,28 +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 self.instance.parent :
deps.append(self.instance.parent)
if datablock.data:
deps.append(datablock.data)
if self.is_library:
deps.append(self.instance.library)
# Particle systems
for particle_slot in datablock.particle_systems:
deps.append(particle_slot.settings)
if self.instance.instance_type == 'COLLECTION':
if datablock.parent:
deps.append(datablock.parent)
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(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

View File

@ -0,0 +1,90 @@
import bpy
import mathutils
from . import dump_anything
from .bl_datablock import BlDatablock, get_datablock_from_uuid
def dump_textures_slots(texture_slots: bpy.types.bpy_prop_collection) -> list:
""" Dump every texture slot collection as the form:
[(index, slot_texture_uuid, slot_texture_name), (), ...]
"""
dumped_slots = []
for index, slot in enumerate(texture_slots):
if slot and slot.texture:
dumped_slots.append((index, slot.texture.uuid, slot.texture.name))
return dumped_slots
def load_texture_slots(dumped_slots: list, target_slots: bpy.types.bpy_prop_collection):
"""
"""
for index, slot in enumerate(target_slots):
if slot:
target_slots.clear(index)
for index, slot_uuid, slot_name in dumped_slots:
target_slots.create(index).texture = get_datablock_from_uuid(
slot_uuid, slot_name
)
IGNORED_ATTR = [
"is_embedded_data",
"is_evaluated",
"is_fluid",
"is_library_indirect",
"users"
]
class BlParticle(BlDatablock):
bl_id = "particles"
bl_class = bpy.types.ParticleSettings
bl_icon = "PARTICLES"
bl_check_common = False
bl_reload_parent = False
def _construct(self, data):
instance = bpy.data.particles.new(data["name"])
instance.uuid = self.uuid
return instance
def _load_implementation(self, data, target):
dump_anything.load(target, data)
dump_anything.load(target.effector_weights, data["effector_weights"])
# Force field
force_field_1 = data.get("force_field_1", None)
if force_field_1:
dump_anything.load(target.force_field_1, force_field_1)
force_field_2 = data.get("force_field_2", None)
if force_field_2:
dump_anything.load(target.force_field_2, force_field_2)
# Texture slots
load_texture_slots(data["texture_slots"], target.texture_slots)
def _dump_implementation(self, data, instance=None):
assert instance
dumper = dump_anything.Dumper()
dumper.depth = 1
dumper.exclude_filter = IGNORED_ATTR
data = dumper.dump(instance)
# Particle effectors
data["effector_weights"] = dumper.dump(instance.effector_weights)
if instance.force_field_1:
data["force_field_1"] = dumper.dump(instance.force_field_1)
if instance.force_field_2:
data["force_field_2"] = dumper.dump(instance.force_field_2)
# Texture slots
data["texture_slots"] = dump_textures_slots(instance.texture_slots)
return data
def _resolve_deps_implementation(self):
return [t.texture for t in self.instance.texture_slots if t and t.texture]

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -610,6 +610,8 @@ class Loader:
instance.write(bpy.data.fonts.get(dump))
elif isinstance(rna_property_type, T.Sound):
instance.write(bpy.data.sounds.get(dump))
# elif isinstance(rna_property_type, T.ParticleSettings):
# instance.write(bpy.data.particles.get(dump))
def _load_matrix(self, matrix, dump):
matrix.write(mathutils.Matrix(dump))

View File

@ -32,6 +32,7 @@ from operator import itemgetter
from pathlib import Path
from queue import Queue
from time import gmtime, strftime
import traceback
try:
import _pickle as pickle
@ -44,11 +45,13 @@ 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
@ -56,6 +59,7 @@ background_execution_queue = Queue()
deleyables = []
stop_modal_executor = False
def session_callback(name):
""" Session callback wrapper
@ -80,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}")
@ -137,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
@ -186,110 +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)
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
# 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.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'},
@ -329,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()
@ -351,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))
@ -600,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"}
@ -629,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"
@ -650,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}")
@ -707,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"
@ -751,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)
@ -795,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"
@ -804,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')
@ -829,9 +808,7 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
)
def execute(self, context):
from replication.graph import ReplicationGraph
# TODO: add filechecks
from replication.repository import Repository
try:
f = gzip.open(self.filepath, "rb")
@ -839,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}")
@ -874,9 +848,9 @@ 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")
utils.clean_scene()
@ -889,15 +863,16 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
for node in graph.list_ordered():
graph[node].apply()
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 = (
@ -920,23 +895,25 @@ classes = (
SessionPurgeOperator,
)
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:
@ -957,18 +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):
if session and session.state['STATE'] == STATE_ACTIVE:
if session and session.state == STATE_ACTIVE:
session.update_user_metadata({
'frame_current': scene.frame_current
})
@ -976,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]
@ -989,44 +967,43 @@ 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
# - if its to someone else, ignore the update
if node and node.owner in [session.id, RP_COMMON]:
if node and (node.owner == session.id or node.bl_check_common):
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)
@ -1035,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

View File

@ -24,7 +24,7 @@ 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
@ -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):
@ -174,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',
@ -195,7 +181,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
connection_timeout: bpy.props.IntProperty(
name='connection timeout',
description='connection timeout before disconnection',
default=1000
default=5000
)
# Replication update settings
depsgraph_update_rate: bpy.props.FloatProperty(
@ -421,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):
@ -516,21 +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
)
def get_preferences():
return bpy.context.preferences.addons[__package__].preferences
classes = (
SessionUser,
@ -549,7 +528,7 @@ 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()

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:

View File

@ -17,13 +17,14 @@
import logging
import sys
import traceback
import bpy
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
STATE_INITIAL, STATE_LOBBY, STATE_QUITTING,
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,24 +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}: {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):
@ -130,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)
@ -144,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:
@ -158,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:
@ -182,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
@ -200,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
@ -233,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,
@ -247,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
@ -262,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

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")

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'

View File

@ -13,7 +13,7 @@ def main():
if len(sys.argv) > 2:
blender_rev = sys.argv[2]
else:
blender_rev = "2.91.0"
blender_rev = "2.92.0"
try:
exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev)

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']

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()

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'])

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

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):

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):

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):

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")

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'])

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):

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):

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'])

View File

@ -5,9 +5,9 @@ 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' modifier because the seed doesn't seems to be
# Removed 'BUILD', 'SOFT_BODY' modifier because the seed doesn't seems to be
# correctly initialized (#TODO: report the bug)
MOFIFIERS_TYPES = [
'DATA_TRANSFER', 'MESH_CACHE', 'MESH_SEQUENCE_CACHE',
@ -22,8 +22,7 @@ MOFIFIERS_TYPES = [
'MESH_DEFORM', 'SHRINKWRAP', 'SIMPLE_DEFORM', 'SMOOTH',
'CORRECTIVE_SMOOTH', 'LAPLACIANSMOOTH', 'SURFACE_DEFORM',
'WARP', 'WAVE', 'CLOTH', 'COLLISION', 'DYNAMIC_PAINT',
'EXPLODE', 'FLUID', 'OCEAN', 'PARTICLE_INSTANCE',
'SOFT_BODY', 'SURFACE']
'EXPLODE', 'FLUID', 'OCEAN', 'PARTICLE_INSTANCE', 'SURFACE']
GP_MODIFIERS_TYPE = [
'GP_ARRAY', 'GP_BUILD', 'GP_MIRROR', 'GP_MULTIPLY',
@ -72,5 +71,5 @@ def test_object(clear_blend):
test = implementation._construct(expected)
implementation._load(expected, test)
result = implementation._dump(test)
print(DeepDiff(expected, result))
assert not DeepDiff(expected, result)

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):

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()

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']

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")

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')