Compare commits

..

2 Commits

Author SHA1 Message Date
48866b74d3 refacctor: remove wrong charaters 2020-07-07 22:35:56 +02:00
d9f1031107 feat: initial version 2020-07-07 22:34:40 +02:00
53 changed files with 1709 additions and 2581 deletions

View File

@ -4,4 +4,5 @@ stages:
include:
- local: .gitlab/ci/test.gitlab-ci.yml
- local: .gitlab/ci/build.gitlab-ci.yml
- local: .gitlab/ci/build.gitlab-ci.yml

View File

@ -1,15 +1,14 @@
build:
stage: build
image: debian:stable-slim
image: python:latest
script:
- git submodule init
- git submodule update
- cd multi_user/libs/replication
- rm -rf tests .git .gitignore script
artifacts:
name: multi_user
paths:
- multi_user
only:
refs:
- master
- develop

View File

@ -1,10 +1,14 @@
test:
stage: test
image: slumber/blender-addon-testing:latest
image: python:latest
script:
- git submodule init
- git submodule update
- apt update
# install blender to get all required dependencies
# TODO: indtall only dependencies
- apt install -f -y gcc python-dev python3.7-dev
- apt install -f -y blender
- python3 -m pip install blender-addon-tester
- python3 scripts/test_addon.py
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.git

View File

@ -37,7 +37,7 @@ All notable changes to this project will be documented in this file.
- Serialization is now based on marshal (2x performance improvements).
- Let pip chose python dependencies install path.
## [0.0.3] - 2020-07-29
## [0.0.3] - Upcoming
### Added
@ -60,29 +60,8 @@ All notable changes to this project will be documented in this file.
- user localization
- repository init
### Removed
- Unused strict right management strategy
- Legacy config management system
## [0.0.4] - preview
### Added
- Dependency graph driven updates [experimental]
- Optional Edit Mode update
- Late join mechanism
- Sync Axis lock replication
- Sync collection offset
- Sync camera orthographic scale
- Logging basic configuration (file output and level)
### Changed
- Auto updater now handle installation from branches
- use uuid for collection loading
### Fixed
- Prevent unsuported datatypes to crash the session
- Modifier vertex group assignation
- Legacy config management system

View File

@ -11,7 +11,7 @@ This tool aims to allow multiple users to work on the same scene over the networ
## Quick installation
1. Download latest release [multi_user.zip](https://gitlab.com/slumber/multi-user/-/jobs/artifacts/master/download?job=build).
1. Download latest release [multi_user.zip](/uploads/8aef79c7cf5b1d9606dc58307fd9ad8b/multi_user.zip).
2. Run blender as administrator (dependencies installation).
3. Install last_version.zip from your addon preferences.
@ -57,16 +57,14 @@ I'm working on it.
| Dependencies | Version | Needed |
| ------------ | :-----: | -----: |
| Replication | latest | yes |
| ZeroMQ | latest | yes |
| JsonDiff | latest | yes |
## Contributing
See [contributing section](https://multi-user.readthedocs.io/en/latest/ways_to_contribute.html) of the documentation.
Feel free to [join the discord server](https://discord.gg/aBPvGws) to chat, seek help and contribute.
## Licensing
See [license](LICENSE)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -8,4 +8,5 @@ Getting started
install
quickstart
known_problems
glossary

View File

@ -0,0 +1,46 @@
.. _known-problems:
==============
Known problems
==============
.. rubric:: What do you need to do in order to use Multi-User through internet?
1. Use Hamachi or ZeroTier (I prefer Hamachi) and create a network.
2. All participants need to join this network.
3. Go to Blender and install Multi-User in the preferneces.
4. Setup and start the session:
* **Host**: After activating Multi-User as an Add-On, press N and go on Multi-User.
Then, put the IP of your network where IP is asked for.
Leave Port and IPC Port on default(5555 and 5561). Increase the Timeout(ms) if the connection is not stable.
Then press on "host".
* **Guest**: After activating Multi-User as an Add-On, press N and go to Multi-User
Then, put the IP of your network where IP is asked for.
Leave Port and IPC Port on default(5555 and 5561)(Simpler, put the same information that the host is using.
BUT,it needs 4 ports for communication. Therefore, you need to put 5555+count of guests [up to 4]. ).
Increase the Timeout(ms) if the connection is not stable. Then press on "connexion".
.. rubric:: What do you need to check if you can't host?
You need to check, if the IP and all ports are correct. If it's not loading, because you laoded a project before hosting, it's not your fault.
Then the version is not sable yet (the project contains data, that is not made stable yet).
.. rubric:: What do you need to check if you can't connect?
Check, if you are connected to the network (VPN) of the host. Also, check if you have all of the information like the host has.
Maybe you have different versions (which shouldn't be the case after Auto-Updater is introduced).
.. rubric:: You are connected, but you dont see anything?
After pressing N, go presence overlay and check the box.
Also, go down and uncheck the box "Show only owned"(unless you need privacy ( ͡° ͜ʖ ͡°) ).
If it's still not working, hit the support channel on the discord channel "multi-user". This little helping text is produced by my own experience
(Ultr-X).
In order to bring attention to other problems, please @ me on the support channel. Every problem brought to me will be documentated to optimize and update this text.
Thank you and have fun with Multi-User, brought to you by "swann".
Here the discord server: https://discord.gg/v5eKgm

View File

@ -299,81 +299,37 @@ Here is a quick list of available actions:
.. _advanced:
Advanced settings
=================
Advanced configuration
======================
This section contains optional settings to configure the session behavior.
.. figure:: img/quickstart_advanced.png
:align: center
Advanced configuration panel
Repository panel
-------
Network
-------
.. figure:: img/quickstart_advanced_network.png
:align: center
Advanced network settings
.. rubric:: Network
**IPC Port** is the port used for Inter Process Communication. This port is used
by the multi-users subprocesses to communicate with each others. If different instances
of the multi-user are using the same IPC port it will create conflict !
.. note::
You only need to modify it if you need to launch multiple clients from the same
computer(or if you try to host and join on the same computer). You should just enter a different
**IPC port** for each blender instance.
You only need to modify it if you need to launch multiple clients from the same
computer(or if you try to host and join on the same computer). You should just 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.
-----------
Replication
-----------
.. figure:: img/quickstart_advanced_replication.png
:align: center
Advanced replication settings
.. rubric:: Replication
**Synchronize render settings** (only host) enable replication of EEVEE and CYCLES render settings to match render between clients.
**Edit Mode Updates** enable objects update while you are in Edit_Mode.
.. warning:: Edit Mode Updates kill performances with complex objects (heavy meshes, gpencil, etc...).
**Update method** allow you to change how replication update are triggered. Until now two update methode are implemented:
- **Default**: Use external threads to monitor datablocks changes, slower and less accurate.
- **Despgraph ⚠️**: Use the blender dependency graph to trigger updates. Faster but experimental and unstable !
**Properties frequency gird** allow to set a custom replication frequency for each type of data-block:
- **Refresh**: pushed data update rate (in second)
- **Apply**: pulled data update rate (in second)
---
Log
---
.. note:: Per-data type settings will soon be revamped for simplification purposes
.. figure:: img/quickstart_advanced_logging.png
:align: center
Advanced log settings
**log level** allow to set the logging level of detail. Here is the detail for each values:
+-----------+-----------------------------------------------+
| Log level | Description |
+===========+===============================================+
| ERROR | Shows only critical error |
+-----------+-----------------------------------------------+
| WARNING | Shows only errors (all kind) |
+-----------+-----------------------------------------------+
| INFO | Shows only status related messages and errors |
+-----------+-----------------------------------------------+
| DEBUG | Shows every possible information. |
+-----------+-----------------------------------------------+

View File

@ -48,6 +48,7 @@ Documentation is organized into the following sections:
getting_started/install
getting_started/quickstart
getting_started/known_problems
getting_started/glossary
.. toctree::

View File

@ -186,24 +186,25 @@ Using a regular command line
You can run the dedicated server on any platform by following those steps:
1. Firstly, download and intall python 3 (3.6 or above).
2. Install the replication library:
2. Download and extract the dedicated server from `here <https://gitlab.com/slumber/replication/-/archive/develop/replication-develop.zip>`_
3. Open a terminal in the extracted folder and install python dependencies by running:
.. code-block:: bash
python -m pip install replication
python -m pip install -r requirements.txt
4. Launch the server with:
4. Launch the server from the same terminal with:
.. code-block:: bash
replication.serve
python scripts/server.py
.. hint::
You can also specify a custom **port** (-p), **timeout** (-t), **admin password** (-pwd), **log level(ERROR, WARNING, INFO or DEBUG)** (-l) and **log file** (-lf) with the following optionnal argument
You can also specify a custom **port** (-p), **timeout** (-t) and **admin password** (-pwd) with the following optionnal argument
.. code-block:: bash
replication.serve -p 5555 -pwd toto -t 1000 -l INFO -lf server.log
python scripts/server.py -p 5555 -pwd toto -t 1000
As soon as the dedicated server is running, you can connect to it from blender (follow :ref:`how-to-join`).

View File

@ -21,7 +21,7 @@ bl_info = {
"author": "Swann Martinez",
"version": (0, 0, 3),
"description": "Enable real-time collaborative workflow inside blender",
"blender": (2, 82, 0),
"blender": (2, 80, 0),
"location": "3D View > Sidebar > Multi-User tab",
"warning": "Unstable addon, use it at your own risks",
"category": "Collaboration",
@ -43,18 +43,24 @@ from bpy.app.handlers import persistent
from . import environment, utils
# TODO: remove dependency as soon as replication will be installed as a module
DEPENDENCIES = {
("replication", '0.0.21a8'),
("zmq","zmq"),
("jsondiff","jsondiff"),
("deepdiff", "deepdiff"),
("psutil","psutil")
}
libs = os.path.dirname(os.path.abspath(__file__))+"\\libs\\replication\\replication"
def register():
# Setup logging policy
logging.basicConfig(
format='%(asctime)s CLIENT %(levelname)-8s %(message)s',
datefmt='%H:%M:%S',
level=logging.INFO)
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
if libs not in sys.path:
sys.path.append(libs)
try:
environment.setup(DEPENDENCIES, bpy.app.binary_path_python)
except ModuleNotFoundError:

View File

@ -23,11 +23,7 @@ https://github.com/CGCookie/blender-addon-updater
"""
__version__ = "1.0.8"
import errno
import traceback
import platform
import ssl
import urllib.request
import urllib
@ -102,7 +98,6 @@ class Singleton_updater(object):
# runtime variables, initial conditions
self._verbose = False
self._use_print_traces = True
self._fake_install = False
self._async_checking = False # only true when async daemon started
self._update_ready = None
@ -138,13 +133,6 @@ class Singleton_updater(object):
self._select_link = select_link_function
# called from except blocks, to print the exception details,
# according to the use_print_traces option
def print_trace():
if self._use_print_traces:
traceback.print_exc()
# -------------------------------------------------------------------------
# Getters and setters
# -------------------------------------------------------------------------
@ -178,7 +166,7 @@ class Singleton_updater(object):
try:
self._auto_reload_post_update = bool(value)
except:
raise ValueError("auto_reload_post_update must be a boolean value")
raise ValueError("Must be a boolean value")
@property
def backup_current(self):
@ -363,7 +351,7 @@ class Singleton_updater(object):
try:
self._repo = str(value)
except:
raise ValueError("repo must be a string value")
raise ValueError("User must be a string")
@property
def select_link(self):
@ -389,7 +377,6 @@ class Singleton_updater(object):
os.makedirs(value)
except:
if self._verbose: print("Error trying to staging path")
self.print_trace()
return
self._updater_path = value
@ -459,16 +446,6 @@ class Singleton_updater(object):
except:
raise ValueError("Verbose must be a boolean value")
@property
def use_print_traces(self):
return self._use_print_traces
@use_print_traces.setter
def use_print_traces(self, value):
try:
self._use_print_traces = bool(value)
except:
raise ValueError("use_print_traces must be a boolean value")
@property
def version_max_update(self):
return self._version_max_update
@ -660,9 +637,6 @@ class Singleton_updater(object):
else:
if self._verbose: print("Tokens not setup for engine yet")
# Always set user agent
request.add_header('User-Agent', "Python/"+str(platform.python_version()))
# run the request
try:
if context:
@ -678,7 +652,6 @@ class Singleton_updater(object):
self._error = "HTTP error"
self._error_msg = str(e.code)
print(self._error, self._error_msg)
self.print_trace()
self._update_ready = None
except urllib.error.URLError as e:
reason = str(e.reason)
@ -690,7 +663,6 @@ class Singleton_updater(object):
self._error = "URL error, check internet connection"
self._error_msg = reason
print(self._error, self._error_msg)
self.print_trace()
self._update_ready = None
return None
else:
@ -712,7 +684,6 @@ class Singleton_updater(object):
self._error_msg = str(e.reason)
self._update_ready = None
print(self._error, self._error_msg)
self.print_trace()
return None
else:
return None
@ -729,17 +700,15 @@ class Singleton_updater(object):
if self._verbose: print("Preparing staging folder for download:\n",local)
if os.path.isdir(local) == True:
try:
shutil.rmtree(local, ignore_errors=True)
shutil.rmtree(local)
os.makedirs(local)
except:
error = "failed to remove existing staging directory"
self.print_trace()
else:
try:
os.makedirs(local)
except:
error = "failed to create staging directory"
self.print_trace()
if error != None:
if self._verbose: print("Error: Aborting update, "+error)
@ -764,10 +733,6 @@ class Singleton_updater(object):
request.add_header('PRIVATE-TOKEN',self._engine.token)
else:
if self._verbose: print("Tokens not setup for selected engine yet")
# Always set user agent
request.add_header('User-Agent', "Python/"+str(platform.python_version()))
self.urlretrieve(urllib.request.urlopen(request,context=context), self._source_zip)
# add additional checks on file size being non-zero
if self._verbose: print("Successfully downloaded update zip")
@ -778,7 +743,6 @@ class Singleton_updater(object):
if self._verbose:
print("Error retrieving download, bad link?")
print("Error: {}".format(e))
self.print_trace()
return False
@ -793,18 +757,16 @@ class Singleton_updater(object):
if os.path.isdir(local):
try:
shutil.rmtree(local, ignore_errors=True)
shutil.rmtree(local)
except:
if self._verbose:print("Failed to removed previous backup folder, contininuing")
self.print_trace()
# remove the temp folder; shouldn't exist but could if previously interrupted
if os.path.isdir(tempdest):
try:
shutil.rmtree(tempdest, ignore_errors=True)
shutil.rmtree(tempdest)
except:
if self._verbose:print("Failed to remove existing temp folder, contininuing")
self.print_trace()
# make the full addon copy, which temporarily places outside the addon folder
if self._backup_ignore_patterns != None:
shutil.copytree(
@ -832,7 +794,7 @@ class Singleton_updater(object):
# make the copy
shutil.move(backuploc,tempdest)
shutil.rmtree(self._addon_root, ignore_errors=True)
shutil.rmtree(self._addon_root)
os.rename(tempdest,self._addon_root)
self._json["backup_date"] = ""
@ -853,7 +815,7 @@ class Singleton_updater(object):
# clear the existing source folder in case previous files remain
outdir = os.path.join(self._updater_path, "source")
try:
shutil.rmtree(outdir, ignore_errors=True)
shutil.rmtree(outdir)
if self._verbose:
print("Source folder cleared")
except:
@ -866,7 +828,6 @@ class Singleton_updater(object):
except Exception as err:
print("Error occurred while making extract dir:")
print(str(err))
self.print_trace()
self._error = "Install failed"
self._error_msg = "Failed to make extract directory"
return -1
@ -908,7 +869,6 @@ class Singleton_updater(object):
if exc.errno != errno.EEXIST:
self._error = "Install failed"
self._error_msg = "Could not create folder from zip"
self.print_trace()
return -1
else:
with open(os.path.join(outdir, subpath), "wb") as outfile:
@ -1002,13 +962,12 @@ class Singleton_updater(object):
print("Clean removing file {}".format(os.path.join(base,f)))
for f in folders:
if os.path.join(base,f)==self._updater_path: continue
shutil.rmtree(os.path.join(base,f), ignore_errors=True)
shutil.rmtree(os.path.join(base,f))
print("Clean removing folder and contents {}".format(os.path.join(base,f)))
except Exception as err:
error = "failed to create clean existing addon folder"
print(error, str(err))
self.print_trace()
# Walk through the base addon folder for rules on pre-removing
# but avoid removing/altering backup and updater file
@ -1024,7 +983,6 @@ class Singleton_updater(object):
if self._verbose: print("Pre-removed file "+file)
except OSError:
print("Failed to pre-remove "+file)
self.print_trace()
# Walk through the temp addon sub folder for replacements
# this implements the overwrite rules, which apply after
@ -1048,7 +1006,7 @@ class Singleton_updater(object):
# otherwise, check each file to see if matches an overwrite pattern
replaced=False
for ptrn in self._overwrite_patterns:
if fnmatch.filter([file],ptrn):
if fnmatch.filter([destFile],ptrn):
replaced=True
break
if replaced:
@ -1064,11 +1022,10 @@ class Singleton_updater(object):
# now remove the temp staging folder and downloaded zip
try:
shutil.rmtree(staging_path, ignore_errors=True)
shutil.rmtree(staging_path)
except:
error = "Error: Failed to remove existing staging directory, consider manually removing "+staging_path
if self._verbose: print(error)
self.print_trace()
def reload_addon(self):
@ -1084,16 +1041,9 @@ class Singleton_updater(object):
# not allowed in restricted context, such as register module
# toggle to refresh
if "addon_disable" in dir(bpy.ops.wm): # 2.7
bpy.ops.wm.addon_disable(module=self._addon_package)
bpy.ops.wm.addon_refresh()
bpy.ops.wm.addon_enable(module=self._addon_package)
print("2.7 reload complete")
else: # 2.8
bpy.ops.preferences.addon_disable(module=self._addon_package)
bpy.ops.preferences.addon_refresh()
bpy.ops.preferences.addon_enable(module=self._addon_package)
print("2.8 reload complete")
bpy.ops.wm.addon_disable(module=self._addon_package)
bpy.ops.wm.addon_refresh()
bpy.ops.wm.addon_enable(module=self._addon_package)
# -------------------------------------------------------------------------
@ -1425,26 +1375,26 @@ class Singleton_updater(object):
if "last_check" not in self._json or self._json["last_check"] == "":
return True
else:
now = datetime.now()
last_check = datetime.strptime(self._json["last_check"],
"%Y-%m-%d %H:%M:%S.%f")
next_check = last_check
offset = timedelta(
days=self._check_interval_days + 30*self._check_interval_months,
hours=self._check_interval_hours,
minutes=self._check_interval_minutes
)
now = datetime.now()
last_check = datetime.strptime(self._json["last_check"],
"%Y-%m-%d %H:%M:%S.%f")
next_check = last_check
offset = timedelta(
days=self._check_interval_days + 30*self._check_interval_months,
hours=self._check_interval_hours,
minutes=self._check_interval_minutes
)
delta = (now - offset) - last_check
if delta.total_seconds() > 0:
if self._verbose:
print("{} Updater: Time to check for updates!".format(self._addon))
return True
if self._verbose:
print("{} Updater: Determined it's not yet time to check for updates".format(self._addon))
return False
delta = (now - offset) - last_check
if delta.total_seconds() > 0:
if self._verbose:
print("{} Updater: Time to check for updates!".format(self._addon))
return True
else:
if self._verbose:
print("{} Updater: Determined it's not yet time to check for updates".format(self._addon))
return False
def get_json_path(self):
"""Returns the full path to the JSON state file used by this updater.
@ -1463,7 +1413,6 @@ class Singleton_updater(object):
except Exception as err:
print("Other OS error occurred while trying to rename old JSON")
print(err)
self.print_trace()
return json_path
def set_updater_json(self):
@ -1564,7 +1513,6 @@ class Singleton_updater(object):
except Exception as exception:
print("Checking for update error:")
print(exception)
self.print_trace()
if not self._error:
self._update_ready = False
self._update_version = None
@ -1676,7 +1624,10 @@ class GitlabEngine(object):
return "{}{}{}".format(self.api_url,"/api/v4/projects/",updater.repo)
def form_tags_url(self, updater):
return "{}{}".format(self.form_repo_url(updater),"/repository/tags")
if updater.use_releases:
return "{}{}".format(self.form_repo_url(updater),"/releases")
else:
return "{}{}".format(self.form_repo_url(updater),"/repository/tags")
def form_branch_list_url(self, updater):
# does not validate branch name.
@ -1704,7 +1655,12 @@ class GitlabEngine(object):
def parse_tags(self, response, updater):
if response == None:
return []
return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response]
# Return asset links from release
if updater.use_releases:
return [{"name": release["name"], "zipball_url": release["assets"]["links"][0]["url"]} for release in response]
else:
return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response]
# -----------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@ -34,13 +34,11 @@ __all__ = [
'bl_metaball',
'bl_lattice',
'bl_lightprobe',
'bl_speaker',
'bl_font',
'bl_sound'
'bl_speaker'
] # Order here defines execution order
from . import *
from replication.data import ReplicatedDataFactory
from ..libs.replication.replication.data import ReplicatedDataFactory
def types_to_register():
return __all__

View File

@ -134,7 +134,6 @@ class BlAction(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'ACTION_TWEAK'
def _construct(self, data):

View File

@ -31,7 +31,6 @@ class BlArmature(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 0
bl_automatic_push = True
bl_check_common = False
bl_icon = 'ARMATURE_DATA'
def _construct(self, data):
@ -93,7 +92,6 @@ class BlArmature(BlDatablock):
new_bone.head = bone_data['head_local']
new_bone.tail_radius = bone_data['tail_radius']
new_bone.head_radius = bone_data['head_radius']
# new_bone.roll = bone_data['roll']
if 'parent' in bone_data:
new_bone.parent = target.edit_bones[data['bones']
@ -125,8 +123,7 @@ class BlArmature(BlDatablock):
'use_connect',
'parent',
'name',
'layers',
# 'roll',
'layers'
]
data = dumper.dump(instance)

View File

@ -29,7 +29,6 @@ class BlCamera(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'CAMERA_DATA'
def _construct(self, data):
@ -37,7 +36,7 @@ class BlCamera(BlDatablock):
def _load_implementation(self, data, target):
loader = Loader()
loader = Loader()
loader.load(target, data)
dof_settings = data.get('dof')
@ -46,22 +45,13 @@ class BlCamera(BlDatablock):
if dof_settings:
loader.load(target.dof, dof_settings)
background_images = data.get('background_images')
if background_images:
target.background_images.clear()
for img_name, img_data in background_images.items():
target_img = target.background_images.new()
target_img.image = bpy.data.images[img_name]
loader.load(target_img, img_data)
def _dump_implementation(self, data, instance=None):
assert(instance)
# TODO: background image support
dumper = Dumper()
dumper.depth = 3
dumper.depth = 2
dumper.include_filter = [
"name",
'type',
@ -80,7 +70,6 @@ class BlCamera(BlDatablock):
'aperture_fstop',
'aperture_blades',
'aperture_rotation',
'ortho_scale',
'aperture_ratio',
'display_size',
'show_limits',
@ -90,24 +79,7 @@ class BlCamera(BlDatablock):
'sensor_fit',
'sensor_height',
'sensor_width',
'show_background_images',
'background_images',
'alpha',
'display_depth',
'frame_method',
'offset',
'rotation',
'scale',
'use_flip_x',
'use_flip_y',
'image'
]
return dumper.dump(instance)
def _resolve_deps_implementation(self):
deps = []
for background in self.instance.background_images:
if background.image:
deps.append(background.image)
return deps

View File

@ -21,55 +21,6 @@ import mathutils
from .. import utils
from .bl_datablock import BlDatablock
from .dump_anything import Loader, Dumper
def dump_collection_children(collection):
collection_children = []
for child in collection.children:
if child not in collection_children:
collection_children.append(child.uuid)
return collection_children
def dump_collection_objects(collection):
collection_objects = []
for object in collection.objects:
if object not in collection_objects:
collection_objects.append(object.uuid)
return collection_objects
def load_collection_objects(dumped_objects, collection):
for object in dumped_objects:
object_ref = utils.find_from_attr('uuid', object, bpy.data.objects)
if object_ref is None:
continue
elif object_ref.name not in collection.objects.keys():
collection.objects.link(object_ref)
for object in collection.objects:
if object.uuid not in dumped_objects:
collection.objects.unlink(object)
def load_collection_childrens(dumped_childrens, collection):
for child_collection in dumped_childrens:
collection_ref = utils.find_from_attr(
'uuid',
child_collection,
bpy.data.collections)
if collection_ref is None:
continue
if collection_ref.name not in collection.children.keys():
collection.children.link(collection_ref)
for child_collection in collection.children:
if child_collection.uuid not in dumped_childrens:
collection.children.unlink(child_collection)
class BlCollection(BlDatablock):
@ -79,47 +30,71 @@ class BlCollection(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = True
def _construct(self, data):
if self.is_library:
with bpy.data.libraries.load(filepath=bpy.data.libraries[self.data['library']].filepath, link=True) as (sourceData, targetData):
targetData.collections = [
name for name in sourceData.collections if name == self.data['name']]
instance = bpy.data.collections[self.data['name']]
return instance
instance = bpy.data.collections.new(data["name"])
return instance
def _load_implementation(self, data, target):
loader = Loader()
loader.load(target, data)
# Load other meshes metadata
target.name = data["name"]
# Objects
load_collection_objects(data['objects'], target)
for object in data["objects"]:
object_ref = bpy.data.objects.get(object)
if object_ref is None:
continue
if object not in target.objects.keys():
target.objects.link(object_ref)
for object in target.objects:
if object.name not in data["objects"]:
target.objects.unlink(object)
# Link childrens
load_collection_childrens(data['children'], target)
for collection in data["children"]:
collection_ref = bpy.data.collections.get(collection)
if collection_ref is None:
continue
if collection_ref.name not in target.children.keys():
target.children.link(collection_ref)
for collection in target.children:
if collection.name not in data["children"]:
target.children.unlink(collection)
def _dump_implementation(self, data, instance=None):
assert(instance)
dumper = Dumper()
dumper.depth = 1
dumper.include_filter = [
"name",
"instance_offset"
]
data = dumper.dump(instance)
data = {}
data['name'] = instance.name
# dump objects
data['objects'] = dump_collection_objects(instance)
collection_objects = []
for object in instance.objects:
if object not in collection_objects:
collection_objects.append(object.name)
data['objects'] = collection_objects
# dump children collections
data['children'] = dump_collection_children(instance)
collection_children = []
for child in instance.children:
if child not in collection_children:
collection_children.append(child.name)
data['children'] = collection_children
return data

View File

@ -52,7 +52,6 @@ class BlCurve(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'CURVE_DATA'
def _construct(self, data):
@ -62,11 +61,6 @@ class BlCurve(BlDatablock):
loader = Loader()
loader.load(target, data)
# if isinstance(curve, T.TextCurve):
# curve.font = data['font']
# curve.font_bold = data['font']
# curve.font_bold_italic = data['font']
# curve.font_italic = data['font']
target.splines.clear()
# load splines
for spline in data['splines'].values():
@ -89,7 +83,6 @@ class BlCurve(BlDatablock):
# new_spline.points[point_index], data['splines'][spline]["points"][point_index])
loader.load(new_spline, spline)
def _dump_implementation(self, data, instance=None):
assert(instance)
dumper = Dumper()
@ -125,17 +118,3 @@ class BlCurve(BlDatablock):
elif isinstance(instance, T.Curve):
data['type'] = 'CURVE'
return data
def _resolve_deps_implementation(self):
# TODO: resolve material
deps = []
curve = self.instance
if isinstance(curve, T.TextCurve):
deps.extend([
curve.font,
curve.font_bold,
curve.font_bold_italic,
curve.font_italic])
return deps

View File

@ -18,12 +18,11 @@
import bpy
import mathutils
import logging
from .. import utils
from .dump_anything import Loader, Dumper
from replication.data import ReplicatedDatablock
from replication.constants import (UP, DIFF_BINARY)
from ..libs.replication.replication.data import ReplicatedDatablock
from ..libs.replication.replication.constants import (UP, DIFF_BINARY)
def has_action(target):
@ -96,14 +95,11 @@ class BlDatablock(ReplicatedDatablock):
bl_delay_apply : refresh rate in sec for apply
bl_automatic_push : boolean
bl_icon : type icon (blender icon name)
bl_check_common: enable check even in common rights
"""
def __init__(self, *args, **kwargs):
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
@ -121,27 +117,15 @@ class BlDatablock(ReplicatedDatablock):
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:
name = self.data.get('name')
logging.debug(f"Constructing {name}")
datablock_ref = self._construct(data=self.data)
datablock_ref = datablock_root.get(
self.data['name'], # Resolve by name
self._construct(data=self.data)) # If it doesn't exist create it
if datablock_ref:
setattr(datablock_ref, 'uuid', self.uuid)
self.instance = datablock_ref
def remove_instance(self):
"""
Remove instance from blender data
"""
assert(self.instance)
datablock_root = getattr(bpy.data, self.bl_id)
datablock_root.remove(self.instance)
def _dump(self, instance=None):
dumper = Dumper()
data = {}
@ -202,7 +186,6 @@ class BlDatablock(ReplicatedDatablock):
if not self.is_library:
dependencies.extend(self._resolve_deps_implementation())
logging.debug(f"{self.instance.name} dependencies: {dependencies}")
return dependencies
def _resolve_deps_implementation(self):

View File

@ -1,166 +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
import mathutils
import logging
import pathlib
import os
from .. import utils
from .dump_anything import Loader, Dumper
from replication.data import ReplicatedDatablock
from replication.constants import (UP, DIFF_BINARY)
class BlFileDatablock(ReplicatedDatablock):
"""BlDatablock
bl_id : blender internal storage identifier
bl_class : blender internal type
bl_delay_refresh : refresh rate in second for observers
bl_delay_apply : refresh rate in sec for apply
bl_automatic_push : boolean
bl_icon : type icon (blender icon name)
bl_check_common: enable check even in common rights
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
self.preferences = utils.get_preferences()
if instance and hasattr(instance, 'uuid'):
instance.uuid = self.uuid
self.diff_method = DIFF_BINARY
def resolve(self):
datablock_ref = None
datablock_root = getattr(bpy.data, self.bl_id)
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
if not datablock_ref:
try:
datablock_ref = datablock_root[self.data['name']]
except Exception:
name = self.data.get('name')
logging.debug(f"Constructing {name}")
datablock_ref = self._construct(data=self.data)
if datablock_ref:
setattr(datablock_ref, 'uuid', self.uuid)
self.instance = datablock_ref
def remove_instance(self):
"""
Remove instance from blender data
"""
assert(self.instance)
datablock_root = getattr(bpy.data, self.bl_id)
datablock_root.remove(self.instance)
def get_filepath(self):
ext = pathlib.Path(self.data['filepath']).suffix
if ext:
name = f"{self.uuid}{ext}"
return os.path.join(self.preferences.cache_directory, name)
else:
return self.data['filepath']
def _construct(self, data):
filepath = self.get_filepath()
# Step 1: load content
if 'file' in data.keys():
self._write_content(data['file'], filepath)
else:
logging.warning("No data to write, skipping.")
# Step 2: construct the file
root = getattr(bpy.data, self.bl_id)
# Step 3: construct the datablock
return root.load(filepath)
def _dump(self, instance=None):
# Step 1: dump related metadata
data = self._dump_metadata(instance=instance)
# Step 2: dump file content
file_content = self._read_content(instance.filepath)
if file_content:
data['file'] = file_content
return data
def _load(self, target, data):
self._load_metadata(target, data)
def _dump_metadata(self, data, target):
"""
Dump datablock metadata
"""
raise NotImplementedError()
def _read_content(self, filepath):
"""
Dump file content
"""
logging.info("Reading file content")
content = None
try:
file = open(bpy.path.abspath(self.instance.filepath), 'rb')
content = file.read()
except IOError:
logging.warning(f"{filepath} doesn't exist, skipping")
else:
file.close()
return content
def _load_metadata(self, target, data):
raise NotImplementedError
def _write_content(self, content, filepath):
"""
Write content on the disk
"""
logging.info("Writing file content")
try:
file = open(filepath, 'wb')
file.write(content)
except IOError:
logging.warning(f"{self.uuid} writing error, skipping.")
else:
file.close()
def resolve_deps(self):
return []
def is_valid(self):
return getattr(bpy.data, self.bl_id).get(self.data['name'])
def diff(self):
return False

View File

@ -1,49 +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
import mathutils
import os
import logging
import pathlib
from .. import utils
from .dump_anything import Loader, Dumper
from .bl_file_datablock import BlFileDatablock
class BlFont(BlFileDatablock):
bl_id = "fonts"
bl_class = bpy.types.VectorFont
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'FILE_FONT'
def _load_metadata(self, data, target):
# No metadate for fonts
pass
def _dump_metadata(self, instance=None):
return {
'filepath': instance.filepath,
'name': instance.name
}
def diff(self):
return False

View File

@ -218,7 +218,6 @@ class BlGpencil(BlDatablock):
bl_delay_refresh = 2
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'GREASEPENCIL'
def _construct(self, data):

View File

@ -24,72 +24,52 @@ from .. import utils
from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock
format_to_ext = {
'BMP': 'bmp',
'IRIS': 'sgi',
'PNG': 'png',
'JPEG': 'jpg',
'JPEG2000': 'jp2',
'TARGA': 'tga',
'TARGA_RAW': 'tga',
'CINEON': 'cin',
'DPX': 'dpx',
'OPEN_EXR_MULTILAYER': 'exr',
'OPEN_EXR': 'exr',
'HDR': 'hdr',
'TIFF': 'tiff',
'AVI_JPEG': 'avi',
'AVI_RAW': 'avi',
'FFMPEG': 'mpeg',
}
def dump_image(image):
pixels = None
if image.source == "GENERATED" or image.packed_file is not None:
prefs = utils.get_preferences()
img_name = f"{image.name}.png"
# Cache the image on the disk
image.filepath_raw = os.path.join(prefs.cache_directory, img_name)
os.makedirs(prefs.cache_directory, exist_ok=True)
image.file_format = "PNG"
image.save()
if image.source == "FILE":
image_path = bpy.path.abspath(image.filepath_raw)
image_directory = os.path.dirname(image_path)
os.makedirs(image_directory, exist_ok=True)
image.save()
file = open(image_path, "rb")
pixels = file.read()
file.close()
else:
raise ValueError()
return pixels
class BlImage(BlDatablock):
bl_id = "images"
bl_class = bpy.types.Image
bl_delay_refresh = 1
bl_delay_refresh = 0
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_automatic_push = False
bl_icon = 'IMAGE_DATA'
def dump_image(self, image):
pixels = None
if image.source == "GENERATED" or image.packed_file is not None:
prefs = utils.get_preferences()
img_name = f"{self.uuid}.{format_to_ext[image.file_format]}"
# Cache the image on the disk
image.filepath_raw = os.path.join(prefs.cache_directory, img_name)
os.makedirs(prefs.cache_directory, exist_ok=True)
image.save()
if image.source == "FILE":
image_path = bpy.path.abspath(image.filepath_raw)
image_directory = os.path.dirname(image_path)
os.makedirs(image_directory, exist_ok=True)
image.save()
file = open(image_path, "rb")
pixels = file.read()
file.close()
else:
raise ValueError()
return pixels
def _construct(self, data):
return bpy.data.images.new(
name=data['name'],
width=data['size'][0],
height=data['size'][1]
)
name=data['name'],
width=data['size'][0],
height=data['size'][1]
)
def _load(self, data, target):
image = target
prefs = utils.get_preferences()
img_format = data['file_format']
img_name = f"{self.uuid}.{format_to_ext[img_format]}"
img_path = os.path.join(prefs.cache_directory, img_name)
img_name = f"{image.name}.png"
img_path = os.path.join(prefs.cache_directory,img_name)
os.makedirs(prefs.cache_directory, exist_ok=True)
file = open(img_path, 'wb')
file.write(data["pixels"])
@ -99,32 +79,27 @@ class BlImage(BlDatablock):
image.filepath = img_path
image.colorspace_settings.name = data["colorspace_settings"]["name"]
loader = Loader()
loader.load(data, target)
def _dump(self, instance=None):
assert(instance)
data = {}
data['pixels'] = self.dump_image(instance)
data['pixels'] = dump_image(instance)
dumper = Dumper()
dumper.depth = 2
dumper.include_filter = [
"name",
'size',
'height',
'alpha',
'float_buffer',
'file_format',
'alpha_mode',
'filepath',
'source',
'colorspace_settings']
dumper.include_filter = [
"name",
'size',
'height',
'alpha',
'float_buffer',
'filepath',
'source',
'colorspace_settings']
data.update(dumper.dump(instance))
return data
def diff(self):
if self.instance and (self.instance.name != self.data['name']):
return True
else:
return False
return False

View File

@ -21,7 +21,7 @@ import mathutils
from .dump_anything import Dumper, Loader, np_dump_collection, np_load_collection
from .bl_datablock import BlDatablock
from replication.exception import ContextError
from ..libs.replication.replication.exception import ContextError
POINT = ['co', 'weight_softbody', 'co_deform']
@ -32,7 +32,6 @@ class BlLattice(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'LATTICE_DATA'
def _construct(self, data):

View File

@ -29,7 +29,6 @@ class BlLibrary(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'LIBRARY_DATA_DIRECT'
def _construct(self, data):

View File

@ -29,7 +29,6 @@ class BlLight(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'LIGHT_DATA'
def _construct(self, data):

View File

@ -30,7 +30,6 @@ class BlLightprobe(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'LIGHTPROBE_GRID'
def _construct(self, data):

View File

@ -19,13 +19,11 @@
import bpy
import mathutils
import logging
import re
from ..utils import get_datablock_from_uuid
from .. import utils
from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
def load_node(node_data, node_tree):
""" Load a node into a node_tree from a dict
@ -38,24 +36,21 @@ def load_node(node_data, node_tree):
loader = Loader()
target_node = node_tree.nodes.new(type=node_data["bl_idname"])
loader.load(target_node, node_data)
image_uuid = node_data.get('image_uuid', None)
loader.load(target_node, node_data)
if image_uuid and not target_node.image:
target_node.image = get_datablock_from_uuid(image_uuid,None)
for input in node_data["inputs"]:
if hasattr(target_node.inputs[input], "default_value"):
try:
target_node.inputs[input].default_value = node_data["inputs"][input]["default_value"]
except:
logging.error(
f"Material {input} parameter not supported, skipping")
logging.error(f"Material {input} parameter not supported, skipping")
def load_links(links_data, node_tree):
""" Load node_tree links from a list
:arg links_data: dumped node links
:type links_data: list
:arg node_tree: node links collection
@ -65,6 +60,7 @@ def load_links(links_data, node_tree):
for link in links_data:
input_socket = node_tree.nodes[link['to_node']].inputs[int(link['to_socket'])]
output_socket = node_tree.nodes[link['from_node']].outputs[int(link['from_socket'])]
node_tree.links.new(input_socket, output_socket)
@ -79,13 +75,11 @@ def dump_links(links):
links_data = []
for link in links:
to_socket = NODE_SOCKET_INDEX.search(link.to_socket.path_from_id()).group(1)
from_socket = NODE_SOCKET_INDEX.search(link.from_socket.path_from_id()).group(1)
links_data.append({
'to_node': link.to_node.name,
'to_socket': to_socket,
'from_node': link.from_node.name,
'from_socket': from_socket,
'to_node':link.to_node.name,
'to_socket':link.to_socket.path_from_id()[-2:-1],
'from_node':link.from_node.name,
'from_socket':link.from_socket.path_from_id()[-2:-1],
})
return links_data
@ -122,10 +116,9 @@ def dump_node(node):
"show_preview",
"show_texture",
"outputs",
"width_hidden",
"image"
"width_hidden"
]
dumped_node = node_dumper.dump(node)
if hasattr(node, 'inputs'):
@ -158,8 +151,7 @@ def dump_node(node):
'location'
]
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
if hasattr(node, 'image') and getattr(node, 'image'):
dumped_node['image_uuid'] = node.image.uuid
return dumped_node
@ -169,7 +161,6 @@ class BlMaterial(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'MATERIAL_DATA'
def _construct(self, data):
@ -185,14 +176,15 @@ class BlMaterial(BlDatablock):
loader.load(
target.grease_pencil, data['grease_pencil'])
if data["use_nodes"]:
if target.node_tree is None:
target.use_nodes = True
target.node_tree.nodes.clear()
loader.load(target, data)
loader.load(target,data)
# Load nodes
for node in data["node_tree"]["nodes"]:
load_node(data["node_tree"]["nodes"][node], target.node_tree)
@ -229,9 +221,9 @@ class BlMaterial(BlDatablock):
for node in instance.node_tree.nodes:
nodes[node.name] = dump_node(node)
data["node_tree"]['nodes'] = nodes
data["node_tree"]["links"] = dump_links(instance.node_tree.links)
if instance.is_grease_pencil:
gp_mat_dumper = Dumper()
gp_mat_dumper.depth = 3
@ -256,7 +248,7 @@ class BlMaterial(BlDatablock):
'texture_clamp',
'gradient_type',
'mix_color',
'flip'
'flip'
]
data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil)
return data
@ -267,9 +259,10 @@ class BlMaterial(BlDatablock):
if self.instance.use_nodes:
for node in self.instance.node_tree.nodes:
if node.type in ['TEX_IMAGE','TEX_ENVIRONMENT']:
if node.type == 'TEX_IMAGE':
deps.append(node.image)
if self.is_library:
deps.append(self.instance.library)
return deps

View File

@ -23,10 +23,11 @@ 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 replication.constants import DIFF_BINARY
from replication.exception import ContextError
from ..libs.replication.replication.constants import DIFF_BINARY
from ..libs.replication.replication.exception import ContextError
from .bl_datablock import BlDatablock
VERTICE = ['co']
EDGE = [
@ -52,7 +53,6 @@ class BlMesh(BlDatablock):
bl_delay_refresh = 2
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'MESH_DATA'
def _construct(self, data):
@ -113,8 +113,8 @@ class BlMesh(BlDatablock):
def _dump_implementation(self, data, instance=None):
assert(instance)
if instance.is_editmode and not self.preferences.enable_editmode_updates:
if instance.is_editmode:
raise ContextError("Mesh is in edit mode")
mesh = instance

View File

@ -68,7 +68,6 @@ class BlMetaball(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'META_BALL'
def _construct(self, data):

View File

@ -16,16 +16,13 @@
# ##### END GPL LICENSE BLOCK #####
import logging
import bpy
import mathutils
from replication.exception import ContextError
import logging
from ..utils import get_datablock_from_uuid
from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock
from .dump_anything import Dumper, Loader
from replication.exception import ReparentException
from ..libs.replication.replication.exception import ContextError
def load_pose(target_bone, data):
@ -34,59 +31,12 @@ def load_pose(target_bone, data):
loader.load(target_bone, data)
def find_data_from_name(name=None):
instance = None
if not name:
pass
elif name in bpy.data.meshes.keys():
instance = bpy.data.meshes[name]
elif name in bpy.data.lights.keys():
instance = bpy.data.lights[name]
elif name in bpy.data.cameras.keys():
instance = bpy.data.cameras[name]
elif name in bpy.data.curves.keys():
instance = bpy.data.curves[name]
elif name in bpy.data.metaballs.keys():
instance = bpy.data.metaballs[name]
elif name in bpy.data.armatures.keys():
instance = bpy.data.armatures[name]
elif name in bpy.data.grease_pencils.keys():
instance = bpy.data.grease_pencils[name]
elif name in bpy.data.curves.keys():
instance = bpy.data.curves[name]
elif name in bpy.data.lattices.keys():
instance = bpy.data.lattices[name]
elif name in bpy.data.speakers.keys():
instance = bpy.data.speakers[name]
elif name in bpy.data.lightprobes.keys():
# Only supported since 2.83
if bpy.app.version[1] >= 83:
instance = bpy.data.lightprobes[name]
else:
logging.warning(
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
return instance
def load_data(object, name):
logging.info("loading data")
pass
def _is_editmode(object: bpy.types.Object) -> bool:
child_data = getattr(object, 'data', None)
return (child_data and
hasattr(child_data, 'is_editmode') and
child_data.is_editmode)
class BlObject(BlDatablock):
bl_id = "objects"
bl_class = bpy.types.Object
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'OBJECT_DATA'
def _construct(self, data):
@ -102,63 +52,43 @@ class BlObject(BlDatablock):
return instance
# TODO: refactoring
object_name = data.get("name")
data_uuid = data.get("data_uuid")
data_id = data.get("data")
object_data = get_datablock_from_uuid(
data_uuid,
find_data_from_name(data_id),
ignore=['images']) #TODO: use resolve_from_id
instance = bpy.data.objects.new(object_name, object_data)
if "data" not in data:
pass
elif data["data"] in bpy.data.meshes.keys():
instance = bpy.data.meshes[data["data"]]
elif data["data"] in bpy.data.lights.keys():
instance = bpy.data.lights[data["data"]]
elif data["data"] in bpy.data.cameras.keys():
instance = bpy.data.cameras[data["data"]]
elif data["data"] in bpy.data.curves.keys():
instance = bpy.data.curves[data["data"]]
elif data["data"] in bpy.data.metaballs.keys():
instance = bpy.data.metaballs[data["data"]]
elif data["data"] in bpy.data.armatures.keys():
instance = bpy.data.armatures[data["data"]]
elif data["data"] in bpy.data.grease_pencils.keys():
instance = bpy.data.grease_pencils[data["data"]]
elif data["data"] in bpy.data.curves.keys():
instance = bpy.data.curves[data["data"]]
elif data["data"] in bpy.data.lattices.keys():
instance = bpy.data.lattices[data["data"]]
elif data["data"] in bpy.data.speakers.keys():
instance = bpy.data.speakers[data["data"]]
elif data["data"] in bpy.data.lightprobes.keys():
# Only supported since 2.83
if bpy.app.version[1] >= 83:
instance = bpy.data.lightprobes[data["data"]]
else:
logging.warning(
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
instance = bpy.data.objects.new(data["name"], instance)
instance.uuid = self.uuid
return instance
def _load_implementation(self, data, target):
loader = Loader()
data_uuid = data.get("data_uuid")
data_id = data.get("data")
if target.type != data['type']:
raise ReparentException()
elif target.data and (target.data.name != data_id):
target.data = get_datablock_from_uuid(data_uuid, find_data_from_name(data_id), ignore=['images'])
# vertex groups
if 'vertex_groups' in data:
target.vertex_groups.clear()
for vg in data['vertex_groups']:
vertex_group=target.vertex_groups.new(name = vg['name'])
point_attr='vertices' if 'vertices' in vg else 'points'
for vert in vg[point_attr]:
vertex_group.add(
[vert['index']], vert['weight'], 'REPLACE')
# SHAPE KEYS
if 'shape_keys' in data:
target.shape_key_clear()
object_data=target.data
# Create keys and load vertices coords
for key_block in data['shape_keys']['key_blocks']:
key_data=data['shape_keys']['key_blocks'][key_block]
target.shape_key_add(name = key_block)
loader.load(
target.data.shape_keys.key_blocks[key_block], key_data)
for vert in key_data['data']:
target.data.shape_keys.key_blocks[key_block].data[vert].co = key_data['data'][vert]['co']
# Load relative key after all
for key_block in data['shape_keys']['key_blocks']:
reference = data['shape_keys']['key_blocks'][key_block]['relative_key']
target.data.shape_keys.key_blocks[key_block].relative_key = target.data.shape_keys.key_blocks[reference]
# Load transformation data
loader = Loader()
loader.load(target, data)
# Pose
@ -184,25 +114,51 @@ class BlObject(BlDatablock):
if 'constraints' in bone_data.keys():
loader.load(target_bone, bone_data['constraints'])
load_pose(target_bone, bone_data)
if 'bone_index' in bone_data.keys():
target_bone.bone_group = target.pose.bone_group[bone_data['bone_group_index']]
# TODO: find another way...
if target.type == 'EMPTY':
img_uuid = data.get('data_uuid')
if target.data is None and img_uuid:
target.data = get_datablock_from_uuid(img_uuid, None)#bpy.data.images.get(img_key, None)
# vertex groups
if 'vertex_groups' in data:
target.vertex_groups.clear()
for vg in data['vertex_groups']:
vertex_group = target.vertex_groups.new(name=vg['name'])
point_attr = 'vertices' if 'vertices' in vg else 'points'
for vert in vg[point_attr]:
vertex_group.add(
[vert['index']], vert['weight'], 'REPLACE')
# SHAPE KEYS
if 'shape_keys' in data:
target.shape_key_clear()
object_data = target.data
# Create keys and load vertices coords
for key_block in data['shape_keys']['key_blocks']:
key_data = data['shape_keys']['key_blocks'][key_block]
target.shape_key_add(name=key_block)
loader.load(
target.data.shape_keys.key_blocks[key_block], key_data)
for vert in key_data['data']:
target.data.shape_keys.key_blocks[key_block].data[vert].co = key_data['data'][vert]['co']
# Load relative key after all
for key_block in data['shape_keys']['key_blocks']:
reference = data['shape_keys']['key_blocks'][key_block]['relative_key']
target.data.shape_keys.key_blocks[key_block].relative_key = target.data.shape_keys.key_blocks[reference]
def _dump_implementation(self, data, instance=None):
assert(instance)
if _is_editmode(instance):
if self.preferences.enable_editmode_updates:
instance.update_from_editmode()
else:
raise ContextError("Object is in edit-mode.")
child_data = getattr(instance, 'data', None)
if child_data and hasattr(child_data, 'is_editmode') and child_data.is_editmode:
raise ContextError("Object is in edit-mode.")
dumper = Dumper()
dumper.depth = 1
@ -215,39 +171,28 @@ class BlObject(BlDatablock):
"library",
"empty_display_type",
"empty_display_size",
"empty_image_offset",
"empty_image_depth",
"empty_image_side",
"show_empty_image_orthographic",
"show_empty_image_perspective",
"show_empty_image_only_axis_aligned",
"use_empty_image_alpha",
"color",
"instance_collection",
"instance_type",
"location",
"scale",
'lock_location',
'lock_rotation',
'lock_scale',
'type',
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
]
data = dumper.dump(instance)
data['data_uuid'] = getattr(instance.data, 'uuid', None)
if self.is_library:
return data
# MODIFIERS
if hasattr(instance, 'modifiers'):
dumper.include_filter = None
dumper.depth = 1
dumper.depth = 2
data["modifiers"] = {}
for index, modifier in enumerate(instance.modifiers):
data["modifiers"][modifier.name] = dumper.dump(modifier)
# CONSTRAINTS
# OBJECT
if hasattr(instance, 'constraints'):
dumper.depth = 3
data["constraints"] = dumper.dump(instance.constraints)
@ -300,8 +245,7 @@ class BlObject(BlDatablock):
# VERTEx GROUP
if len(instance.vertex_groups) > 0:
points_attr = 'vertices' if isinstance(
instance.data, bpy.types.Mesh) else 'points'
points_attr = 'vertices' if isinstance(instance.data, bpy.types.Mesh) else 'points'
vg_data = []
for vg in instance.vertex_groups:
vg_idx = vg.index
@ -356,7 +300,7 @@ class BlObject(BlDatablock):
def _resolve_deps_implementation(self):
deps = []
# Avoid Empty case
if self.instance.data:
deps.append(self.instance.data)
@ -371,3 +315,4 @@ class BlObject(BlDatablock):
deps.append(self.instance.instance_collection)
return deps

View File

@ -21,7 +21,7 @@ import mathutils
from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock
from .bl_collection import dump_collection_children, dump_collection_objects, load_collection_childrens, load_collection_objects
from ..utils import get_preferences
class BlScene(BlDatablock):
@ -30,7 +30,6 @@ class BlScene(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = True
bl_icon = 'SCENE_DATA'
def _construct(self, data):
@ -43,8 +42,24 @@ class BlScene(BlDatablock):
loader.load(target, data)
# Load master collection
load_collection_objects(data['collection']['objects'], target.collection)
load_collection_childrens(data['collection']['children'], target.collection)
for object in data["collection"]["objects"]:
if object not in target.collection.objects.keys():
target.collection.objects.link(bpy.data.objects[object])
for object in target.collection.objects.keys():
if object not in data["collection"]["objects"]:
target.collection.objects.unlink(bpy.data.objects[object])
# load collections
for collection in data["collection"]["children"]:
if collection not in target.collection.children.keys():
target.collection.children.link(
bpy.data.collections[collection])
for collection in target.collection.children.keys():
if collection not in data["collection"]["children"]:
target.collection.children.unlink(
bpy.data.collections[collection])
if 'world' in data.keys():
target.world = bpy.data.worlds[data['world']]
@ -59,9 +74,6 @@ class BlScene(BlDatablock):
if 'cycles' in data.keys():
loader.load(target.eevee, data['cycles'])
if 'render' in data.keys():
loader.load(target.render, data['render'])
if 'view_settings' in data.keys():
loader.load(target.view_settings, data['view_settings'])
if target.view_settings.use_curve_mapping:
@ -82,18 +94,13 @@ class BlScene(BlDatablock):
'id',
'camera',
'grease_pencil',
'frame_start',
'frame_end',
'frame_step',
]
data = scene_dumper.dump(instance)
scene_dumper.depth = 3
scene_dumper.include_filter = ['children','objects','name']
data['collection'] = {}
data['collection']['children'] = dump_collection_children(instance.collection)
data['collection']['objects'] = dump_collection_objects(instance.collection)
data['collection'] = scene_dumper.dump(instance.collection)
scene_dumper.depth = 1
scene_dumper.include_filter = None
@ -119,8 +126,7 @@ class BlScene(BlDatablock):
data['eevee'] = scene_dumper.dump(instance.eevee)
data['cycles'] = scene_dumper.dump(instance.cycles)
data['view_settings'] = scene_dumper.dump(instance.view_settings)
data['render'] = scene_dumper.dump(instance.render)
if instance.view_settings.use_curve_mapping:
data['view_settings']['curve_mapping'] = scene_dumper.dump(instance.view_settings.curve_mapping)
scene_dumper.depth = 5

View File

@ -1,74 +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
import mathutils
import os
import logging
import pathlib
from .. import utils
from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock
class BlSound(BlDatablock):
bl_id = "sounds"
bl_class = bpy.types.Sound
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'SOUND'
def _construct(self, data):
if 'file' in data.keys():
prefs = utils.get_preferences()
ext = data['filepath'].split(".")[-1]
sound_name = f"{self.uuid}.{ext}"
sound_path = os.path.join(prefs.cache_directory, sound_name)
os.makedirs(prefs.cache_directory, exist_ok=True)
file = open(sound_path, 'wb')
file.write(data["file"])
file.close()
logging.info(f'loading {sound_path}')
return bpy.data.sounds.load(sound_path)
def _load(self, data, target):
loader = Loader()
loader.load(target, data)
def _dump(self, instance=None):
if not instance.packed_file:
# prefs = utils.get_preferences()
# ext = pathlib.Path(instance.filepath).suffix
# sound_name = f"{self.uuid}{ext}"
# sound_path = os.path.join(prefs.cache_directory, sound_name)
# instance.filepath = sound_path
instance.pack()
#TODO:use file locally with unpack(method='USE_ORIGINAL') ?
return {
'filepath':instance.filepath,
'name':instance.name,
'file': instance.packed_file.data
}
def diff(self):
return False

View File

@ -29,7 +29,6 @@ class BlSpeaker(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = False
bl_icon = 'SPEAKER'
def _load_implementation(self, data, target):
@ -49,7 +48,6 @@ class BlSpeaker(BlDatablock):
'volume',
'name',
'pitch',
'sound',
'volume_min',
'volume_max',
'attenuation',
@ -62,15 +60,6 @@ class BlSpeaker(BlDatablock):
return dumper.dump(instance)
def _resolve_deps_implementation(self):
# TODO: resolve material
deps = []
sound = self.instance.sound
if sound:
deps.append(sound)
return deps

View File

@ -30,7 +30,6 @@ class BlWorld(BlDatablock):
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_check_common = True
bl_icon = 'WORLD_DATA'
def _construct(self, data):
@ -56,14 +55,19 @@ class BlWorld(BlDatablock):
assert(instance)
world_dumper = Dumper()
world_dumper.depth = 1
world_dumper.include_filter = [
"use_nodes",
"name",
world_dumper.depth = 2
world_dumper.exclude_filter = [
"preview",
"original",
"uuid",
"color",
"cycles",
"light_settings",
"users",
"view_center"
]
data = world_dumper.dump(instance)
if instance.use_nodes:
data['node_tree'] = {}
nodes = {}
for node in instance.node_tree.nodes:
@ -80,7 +84,7 @@ class BlWorld(BlDatablock):
if self.instance.use_nodes:
for node in self.instance.node_tree.nodes:
if node.type in ['TEX_IMAGE','TEX_ENVIRONMENT']:
if node.type == 'TEX_IMAGE':
deps.append(node.image)
if self.is_library:
deps.append(self.instance.library)

View File

@ -115,7 +115,7 @@ def np_dump_collection_primitive(collection: bpy.types.CollectionProperty, attri
:return: numpy byte buffer
"""
if len(collection) == 0:
logging.debug(f'Skipping empty {attribute} attribute')
logging.warning(f'Skipping empty {attribute} attribute')
return {}
attr_infos = collection[0].bl_rna.properties.get(attribute)
@ -192,7 +192,7 @@ def np_load_collection_primitives(collection: bpy.types.CollectionProperty, attr
:type sequence: strr
"""
if len(collection) == 0 or not sequence:
logging.debug(f"Skipping loading {attribute}")
logging.warning(f"Skipping loadin {attribute}")
return
attr_infos = collection[0].bl_rna.properties.get(attribute)
@ -301,7 +301,7 @@ class Dumper:
self._dump_ID = (lambda x, depth: x.name, self._dump_default_as_branch)
self._dump_collection = (
self._dump_default_as_leaf, self._dump_collection_as_branch)
self._dump_array = (self._dump_array_as_branch,
self._dump_array = (self._dump_default_as_leaf,
self._dump_array_as_branch)
self._dump_matrix = (self._dump_matrix_as_leaf,
self._dump_matrix_as_leaf)
@ -593,10 +593,6 @@ class Loader:
instance.write(bpy.data.materials.get(dump))
elif isinstance(rna_property_type, T.Collection):
instance.write(bpy.data.collections.get(dump))
elif isinstance(rna_property_type, T.VectorFont):
instance.write(bpy.data.fonts.get(dump))
elif isinstance(rna_property_type, T.Sound):
instance.write(bpy.data.sounds.get(dump))
def _load_matrix(self, matrix, dump):
matrix.write(mathutils.Matrix(dump))

View File

@ -20,16 +20,14 @@ import logging
import bpy
from . import operators, presence, utils
from replication.constants import (FETCHED,
UP,
RP_COMMON,
STATE_INITIAL,
STATE_QUITTING,
STATE_ACTIVE,
STATE_SYNCING,
STATE_LOBBY,
STATE_SRV_SYNC,
REPARENT)
from .libs.replication.replication.constants import (FETCHED,
RP_COMMON,
STATE_INITIAL,
STATE_QUITTING,
STATE_ACTIVE,
STATE_SYNCING,
STATE_LOBBY,
STATE_SRV_SYNC)
class Delayable():
@ -89,28 +87,16 @@ class ApplyTimer(Timer):
def execute(self):
client = operators.client
if client and client.state['STATE'] == STATE_ACTIVE:
if self._type:
nodes = client.list(filter=self._type)
else:
nodes = client.list()
nodes = client.list(filter=self._type)
for node in nodes:
node_ref = client.get(uuid=node)
if node_ref.state == FETCHED:
try:
client.apply(node, force=True)
client.apply(node)
except Exception as e:
logging.error(f"Fail to apply {node_ref.uuid}: {e}")
elif node_ref.state == REPARENT:
# Reload the node
node_ref.remove_instance()
node_ref.resolve()
client.apply(node, force=True)
for parent in client._graph.find_parents(node):
logging.info(f"Applying parent {parent}")
client.apply(parent, force=True)
node_ref.state = UP
class DynamicRightSelectTimer(Timer):
@ -253,7 +239,7 @@ class DrawClient(Draw):
class ClientUpdate(Timer):
def __init__(self, timout=.1):
def __init__(self, timout=.016):
super().__init__(timout)
self.handle_quit = False
self.users_metadata = {}
@ -265,16 +251,14 @@ class ClientUpdate(Timer):
if session and renderer:
if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]:
local_user = operators.client.online_users.get(
settings.username)
local_user = operators.client.online_users.get(settings.username)
if not local_user:
return
else:
for username, user_data in operators.client.online_users.items():
if username != settings.username:
cached_user_data = self.users_metadata.get(
username)
cached_user_data = self.users_metadata.get(username)
new_user_data = operators.client.online_users[username]['metadata']
if cached_user_data is None:
@ -288,7 +272,7 @@ class ClientUpdate(Timer):
local_user_metadata = local_user.get('metadata')
scene_current = bpy.context.scene.name
local_user = session.online_users.get(settings.username)
local_user = session.online_users.get(settings.username)
current_view_corners = presence.get_view_corners()
# Init client metadata
@ -297,9 +281,9 @@ class ClientUpdate(Timer):
'view_corners': presence.get_view_matrix(),
'view_matrix': presence.get_view_matrix(),
'color': (settings.client_color.r,
settings.client_color.g,
settings.client_color.b,
1),
settings.client_color.g,
settings.client_color.b,
1),
'frame_current': bpy.context.scene.frame_current,
'scene_current': scene_current
}
@ -312,41 +296,33 @@ class ClientUpdate(Timer):
session.update_user_metadata(local_user_metadata)
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']:
local_user_metadata['view_corners'] = current_view_corners
local_user_metadata['view_matrix'] = presence.get_view_matrix(
)
local_user_metadata['view_matrix'] = presence.get_view_matrix()
session.update_user_metadata(local_user_metadata)
# sync online users
session_users = operators.client.online_users
ui_users = bpy.context.window_manager.online_users
for index, user in enumerate(ui_users):
if user.username not in session_users.keys():
ui_users.remove(index)
renderer.flush_selection()
renderer.flush_users()
break
class SessionStatusUpdate(Timer):
def __init__(self, timout=1):
super().__init__(timout)
for user in session_users:
if user not in ui_users:
new_key = ui_users.add()
new_key.name = user
new_key.username = user
elif session.state['STATE'] == STATE_QUITTING:
presence.refresh_sidebar_view()
self.handle_quit = True
elif session.state['STATE'] == STATE_INITIAL and self.handle_quit:
self.handle_quit = False
presence.refresh_sidebar_view()
def execute(self):
presence.refresh_sidebar_view()
operators.unregister_delayables()
presence.renderer.stop()
class SessionUserSync(Timer):
def __init__(self, timout=1):
super().__init__(timout)
def execute(self):
session = getattr(operators, 'client', None)
renderer = getattr(presence, 'renderer', None)
if session and renderer:
# sync online users
session_users = operators.client.online_users
ui_users = bpy.context.window_manager.online_users
for index, user in enumerate(ui_users):
if user.username not in session_users.keys():
ui_users.remove(index)
renderer.flush_selection()
renderer.flush_users()
break
for user in session_users:
if user not in ui_users:
new_key = ui_users.add()
new_key.name = user
new_key.username = user
presence.refresh_sidebar_view()

View File

@ -23,9 +23,6 @@ import subprocess
import sys
from pathlib import Path
import socket
import re
VERSION_EXPR = re.compile('\d+\.\d+\.\d+\w\d+')
THIRD_PARTY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libs")
DEFAULT_CACHE_DIR = os.path.join(
@ -50,29 +47,10 @@ def install_pip():
subprocess.run([str(PYTHON_PATH), "-m", "ensurepip"])
def install_package(name, version):
logging.info(f"installing {name} version...")
env = os.environ
if "PIP_REQUIRE_VIRTUALENV" in env:
# PIP_REQUIRE_VIRTUALENV is an env var to ensure pip cannot install packages outside a virtual env
# https://docs.python-guide.org/dev/pip-virtualenv/
# But since Blender's pip is outside of a virtual env, it can block our packages installation, so we unset the
# env var for the subprocess.
env = os.environ.copy()
del env["PIP_REQUIRE_VIRTUALENV"]
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", f"{name}=={version}"], env=env)
def install_package(name):
logging.debug(f"Using {PYTHON_PATH} for installation")
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", name])
def check_package_version(name, required_version):
logging.info(f"Checking {name} version...")
out = subprocess.run(f"{str(PYTHON_PATH)} -m pip show {name}", capture_output=True)
version = VERSION_EXPR.search(out.stdout.decode())
if version and version.group() == required_version:
logging.info(f"{name} is up to date")
return True
else:
logging.info(f"{name} need an update")
return False
def get_ip():
"""
@ -100,9 +78,7 @@ def setup(dependencies, python_path):
if not module_can_be_imported("pip"):
install_pip()
for package_name, package_version in dependencies:
if not module_can_be_imported(package_name):
install_package(package_name, package_version)
for module_name, package_name in dependencies:
if not module_can_be_imported(module_name):
install_package(package_name)
module_can_be_imported(package_name)
elif not check_package_version(package_name, package_version):
install_package(package_name, package_version)

View File

View File

@ -33,19 +33,31 @@ import mathutils
from bpy.app.handlers import persistent
from . import bl_types, delayable, environment, presence, ui, utils
from replication.constants import (FETCHED, STATE_ACTIVE,
STATE_INITIAL,
STATE_SYNCING, RP_COMMON, UP)
from replication.data import ReplicatedDataFactory
from replication.exception import NonAuthorizedOperationError
from replication.interface import Session
from .libs.replication.replication.constants import (FETCHED, STATE_ACTIVE,
STATE_INITIAL,
STATE_SYNCING)
from .libs.replication.replication.data import ReplicatedDataFactory
from .libs.replication.replication.exception import NonAuthorizedOperationError
from .libs.replication.replication.interface import Session
client = None
delayables = []
stop_modal_executor = False
modal_executor_queue = None
def unregister_delayables():
global delayables, stop_modal_executor
for d in delayables:
try:
d.unregister()
except:
continue
stop_modal_executor = True
# OPERATORS
@ -67,31 +79,10 @@ class SessionStartOperator(bpy.types.Operator):
runtime_settings = context.window_manager.session
users = bpy.data.window_managers['WinMan'].online_users
admin_pass = runtime_settings.password
use_extern_update = settings.update_method == 'DEPSGRAPH'
unregister_delayables()
users.clear()
delayables.clear()
logger = logging.getLogger()
if len(logger.handlers)==1:
formatter = logging.Formatter(
fmt='%(asctime)s CLIENT %(levelname)-8s %(message)s',
datefmt='%H:%M:%S'
)
log_directory = os.path.join(
settings.cache_directory,
"multiuser_client.log")
os.makedirs(settings.cache_directory, exist_ok=True)
handler = logging.FileHandler(log_directory, mode='w')
logger.addHandler(handler)
for handler in logger.handlers:
if isinstance(handler, logging.NullHandler):
continue
handler.setFormatter(formatter)
bpy_factory = ReplicatedDataFactory()
supported_bl_types = []
@ -110,25 +101,18 @@ class SessionStartOperator(bpy.types.Operator):
bpy_factory.register_type(
type_module_class.bl_class,
type_module_class,
timer=type_local_config.bl_delay_refresh*1000,
automatic=type_local_config.auto_push,
check_common=type_module_class.bl_check_common)
timer=type_local_config.bl_delay_refresh,
automatic=type_local_config.auto_push)
if settings.update_method == 'DEFAULT':
if type_local_config.bl_delay_apply > 0:
delayables.append(
delayable.ApplyTimer(
timout=type_local_config.bl_delay_apply,
target_type=type_module_class))
if type_local_config.bl_delay_apply > 0:
delayables.append(
delayable.ApplyTimer(
timout=type_local_config.bl_delay_apply,
target_type=type_module_class))
client = Session(
factory=bpy_factory,
python_path=bpy.app.binary_path_python,
external_update_handling=use_extern_update)
if settings.update_method == 'DEPSGRAPH':
delayables.append(delayable.ApplyTimer(
settings.depsgraph_update_rate/1000))
python_path=bpy.app.binary_path_python)
# Host a session
if self.host:
@ -147,10 +131,7 @@ class SessionStartOperator(bpy.types.Operator):
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),
password=admin_pass
)
except Exception as e:
self.report({'ERROR'}, repr(e))
@ -177,62 +158,21 @@ class SessionStartOperator(bpy.types.Operator):
logging.error(str(e))
# Background client updates service
#TODO: Refactoring
delayables.append(delayable.ClientUpdate())
delayables.append(delayable.DrawClient())
delayables.append(delayable.DynamicRightSelectTimer())
session_update = delayable.SessionStatusUpdate()
session_user_sync = delayable.SessionUserSync()
session_update.register()
session_user_sync.register()
# Launch drawing module
if runtime_settings.enable_presence:
presence.renderer.run()
delayables.append(session_update)
delayables.append(session_user_sync)
@client.register('on_connection')
def initialize_session():
settings = utils.get_preferences()
for node in client._graph.list_ordered():
node_ref = client.get(node)
if node_ref.state == FETCHED:
node_ref.resolve()
for node in client._graph.list_ordered():
node_ref = client.get(node)
if node_ref.state == FETCHED:
node_ref.apply()
# Launch drawing module
if runtime_settings.enable_presence:
presence.renderer.run()
# Register blender main thread tools
for d in delayables:
d.register()
if settings.update_method == 'DEPSGRAPH':
bpy.app.handlers.depsgraph_update_post.append(
depsgraph_evaluation)
@client.register('on_exit')
def desinitialize_session():
global delayables, stop_modal_executor
settings = utils.get_preferences()
for d in delayables:
try:
d.unregister()
except:
continue
stop_modal_executor = True
presence.renderer.stop()
if settings.update_method == 'DEPSGRAPH':
bpy.app.handlers.depsgraph_update_post.remove(
depsgraph_evaluation)
# Register blender main thread tools
for d in delayables:
d.register()
global modal_executor_queue
modal_executor_queue = queue.Queue()
bpy.ops.session.apply_armature_operator()
self.report(
@ -449,16 +389,14 @@ class SessionSnapUserOperator(bpy.types.Operator):
if target_scene != context.scene.name:
blender_scene = bpy.data.scenes.get(target_scene, None)
if blender_scene is None:
self.report(
{'ERROR'}, f"Scene {target_scene} doesn't exist on the local client.")
self.report({'ERROR'}, f"Scene {target_scene} doesn't exist on the local client.")
session_sessings.time_snap_running = False
return {"CANCELLED"}
bpy.context.window.scene = blender_scene
# Update client viewmatrix
client_vmatrix = target_ref['metadata'].get(
'view_matrix', None)
client_vmatrix = target_ref['metadata'].get('view_matrix', None)
if client_vmatrix:
rv3d.view_matrix = mathutils.Matrix(client_vmatrix)
@ -589,7 +527,7 @@ class ApplyArmatureOperator(bpy.types.Operator):
try:
client.apply(node)
except Exception as e:
logging.error("Fail to apply armature: {e}")
logging.error("Dail to apply armature: {e}")
return {'PASS_THROUGH'}
@ -655,41 +593,6 @@ def update_client_frame(scene):
})
@persistent
def depsgraph_evaluation(scene):
if client and client.state['STATE'] == STATE_ACTIVE:
context = bpy.context
blender_depsgraph = bpy.context.view_layer.depsgraph
dependency_updates = [u for u in blender_depsgraph.updates]
settings = utils.get_preferences()
# NOTE: maybe we don't need to check each update but only the first
for update in reversed(dependency_updates):
# Is the object tracked ?
if update.id.uuid:
# Retrieve local version
node = client.get(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 (go deeper ?)
if node and node.owner in [client.id, RP_COMMON] and node.state == UP:
# Avoid slow geometry update
if 'EDIT' in context.mode and \
not settings.enable_editmode_updates:
break
client.stash(node.uuid)
else:
# Distant update
continue
# else:
# # New items !
# logger.error("UPDATE: ADD")
def register():
from bpy.utils import register_class
for cls in classes:
@ -718,3 +621,7 @@ def unregister():
bpy.app.handlers.load_pre.remove(load_pre_handler)
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
if __name__ == "__main__":
register()

View File

@ -21,9 +21,8 @@ import bpy
import string
import re
from . import bl_types, environment, addon_updater_ops, presence, ui
from .utils import get_preferences, get_expanded_icon
from replication.constants import RP_COMMON
from . import utils, bl_types, environment, addon_updater_ops, presence, ui
from .libs.replication.replication.constants import RP_COMMON
IP_EXPR = re.compile('\d+\.\d+\.\d+\.\d+')
@ -47,7 +46,6 @@ def update_panel_category(self, context):
ui.SESSION_PT_settings.bl_category = self.panel_category
ui.register()
def update_ip(self, context):
ip = IP_EXPR.search(self.ip)
@ -57,25 +55,14 @@ def update_ip(self, context):
logging.error("Wrong IP format")
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 conflic with the port, assigning a random value")
self['ipc_port'] >= self.port:
logging.error("IPC Port in conflic with the port, assigning a random value")
self['ipc_port'] = random.randrange(self.port+4, 10000)
def set_log_level(self, value):
logging.getLogger().setLevel(value)
def get_log_level(self):
return logging.getLogger().level
class ReplicatedDatablock(bpy.types.PropertyGroup):
type_name: bpy.props.StringProperty()
bl_name: bpy.props.StringProperty()
@ -142,26 +129,6 @@ class SessionPrefs(bpy.types.AddonPreferences):
description='connection timeout before disconnection',
default=1000
)
update_method: bpy.props.EnumProperty(
name='update method',
description='replication update method',
items=[
('DEFAULT', "Default", "Default: Use threads to monitor databloc changes"),
('DEPSGRAPH', "Depsgraph",
"Experimental: Use the blender dependency graph to trigger updates"),
],
)
# Replication update settings
depsgraph_update_rate: bpy.props.IntProperty(
name='depsgraph update rate',
description='Dependency graph uppdate rate (milliseconds)',
default=100
)
enable_editmode_updates: bpy.props.BoolProperty(
name="Edit mode updates",
description="Enable objects update in edit mode (! Impact performances !)",
default=False
)
# for UI
category: bpy.props.EnumProperty(
name="Category",
@ -172,18 +139,17 @@ class SessionPrefs(bpy.types.AddonPreferences):
],
default='CONFIG'
)
# WIP
logging_level: bpy.props.EnumProperty(
name="Log level",
description="Log verbosity level",
items=[
('ERROR', "error", "show only errors", logging.ERROR),
('WARNING', "warning", "only show warnings and errors", logging.WARNING),
('INFO', "info", "default level", logging.INFO),
('DEBUG', "debug", "show all logs", logging.DEBUG),
('ERROR', "error", "show only errors"),
('WARNING', "warning", "only show warnings and errors"),
('INFO', "info", "default level"),
('DEBUG', "debug", "show all logs"),
],
default='INFO',
set=set_log_level,
get=get_log_level
default='INFO'
)
conf_session_identity_expanded: bpy.props.BoolProperty(
name="Identity",
@ -215,21 +181,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
description="Interface",
default=False
)
sidebar_advanced_rep_expanded: bpy.props.BoolProperty(
name="sidebar_advanced_rep_expanded",
description="sidebar_advanced_rep_expanded",
default=False
)
sidebar_advanced_log_expanded: bpy.props.BoolProperty(
name="sidebar_advanced_log_expanded",
description="sidebar_advanced_log_expanded",
default=False
)
sidebar_advanced_net_expanded: bpy.props.BoolProperty(
name="sidebar_advanced_net_expanded",
description="sidebar_advanced_net_expanded",
default=False
)
auto_check_update: bpy.props.BoolProperty(
name="Auto-check for Update",
description="If enabled, auto-check for updates using an interval",
@ -281,8 +233,8 @@ class SessionPrefs(bpy.types.AddonPreferences):
box = grid.box()
box.prop(
self, "conf_session_identity_expanded", text="User informations",
icon=get_expanded_icon(self.conf_session_identity_expanded),
emboss=False)
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_identity_expanded
else 'DISCLOSURE_TRI_RIGHT', emboss=False)
if self.conf_session_identity_expanded:
box.row().prop(self, "username", text="name")
box.row().prop(self, "client_color", text="color")
@ -291,26 +243,23 @@ class SessionPrefs(bpy.types.AddonPreferences):
box = grid.box()
box.prop(
self, "conf_session_net_expanded", text="Netorking",
icon=get_expanded_icon(self.conf_session_net_expanded),
emboss=False)
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_net_expanded
else 'DISCLOSURE_TRI_RIGHT', emboss=False)
if self.conf_session_net_expanded:
box.row().prop(self, "ip", text="Address")
row = box.row()
row.label(text="Port:")
row.prop(self, "port", text="")
row.prop(self, "port", text="Address")
row = box.row()
row.label(text="Init the session from:")
row.prop(self, "init_method", text="")
row = box.row()
row.label(text="Update method:")
row.prop(self, "update_method", text="")
table = box.box()
table.row().prop(
self, "conf_session_timing_expanded", text="Refresh rates",
icon=get_expanded_icon(self.conf_session_timing_expanded),
emboss=False)
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_timing_expanded
else 'DISCLOSURE_TRI_RIGHT', emboss=False)
if self.conf_session_timing_expanded:
line = table.row()
@ -328,8 +277,8 @@ class SessionPrefs(bpy.types.AddonPreferences):
box = grid.box()
box.prop(
self, "conf_session_hosting_expanded", text="Hosting",
icon=get_expanded_icon(self.conf_session_hosting_expanded),
emboss=False)
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_hosting_expanded
else 'DISCLOSURE_TRI_RIGHT', emboss=False)
if self.conf_session_hosting_expanded:
row = box.row()
row.label(text="Init the session from:")
@ -339,8 +288,8 @@ class SessionPrefs(bpy.types.AddonPreferences):
box = grid.box()
box.prop(
self, "conf_session_cache_expanded", text="Cache",
icon=get_expanded_icon(self.conf_session_cache_expanded),
emboss=False)
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_cache_expanded
else 'DISCLOSURE_TRI_RIGHT', emboss=False)
if self.conf_session_cache_expanded:
box.row().prop(self, "cache_directory", text="Cache directory")
@ -348,14 +297,14 @@ class SessionPrefs(bpy.types.AddonPreferences):
box = grid.box()
box.prop(
self, "conf_session_ui_expanded", text="Interface",
icon=get_expanded_icon(self.conf_session_ui_expanded),
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_ui_expanded else 'DISCLOSURE_TRI_RIGHT',
emboss=False)
if self.conf_session_ui_expanded:
box.row().prop(self, "panel_category", text="Panel category", expand=True)
if self.category == 'UPDATE':
from . import addon_updater_ops
addon_updater_ops.update_settings_ui(self, context)
addon_updater_ops.update_settings_ui_condensed(self, context)
def generate_supported_types(self):
self.supported_datablocks.clear()
@ -382,7 +331,7 @@ def client_list_callback(scene, context):
items = [(RP_COMMON, RP_COMMON, "")]
username = get_preferences().username
username = utils.get_preferences().username
cli = operators.client
if cli:
client_ids = cli.online_users.keys()

View File

@ -19,7 +19,6 @@
import copy
import logging
import math
import traceback
import bgl
import blf
@ -61,8 +60,7 @@ def refresh_sidebar_view():
"""
area, region, rv3d = view3d_find()
if area:
area.regions[3].tag_redraw()
area.regions[3].tag_redraw()
def get_target(region, rv3d, coord):
target = [0, 0, 0]
@ -313,10 +311,10 @@ class DrawFactory(object):
self.d2d_items[client_id] = (position[1], client_id, color)
except Exception as e:
logging.debug(f"Draw client exception: {e} \n {traceback.format_exc()}\n pos:{position},ind:{indices}")
logging.error(f"Draw client exception: {e}")
def draw3d_callback(self):
bgl.glLineWidth(2.)
bgl.glLineWidth(1.5)
bgl.glEnable(bgl.GL_DEPTH_TEST)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)

View File

@ -18,9 +18,8 @@
import bpy
from . import operators
from .utils import get_preferences, get_expanded_icon
from replication.constants import (ADDED, ERROR, FETCHED,
from . import operators, utils
from .libs.replication.replication.constants import (ADDED, ERROR, FETCHED,
MODIFIED, RP_COMMON, UP,
STATE_ACTIVE, STATE_AUTH,
STATE_CONFIG, STATE_SYNCING,
@ -28,7 +27,6 @@ from replication.constants import (ADDED, ERROR, FETCHED,
STATE_WAITING, STATE_QUITTING,
STATE_LOBBY,
STATE_LAUNCHING_SERVICES)
from replication import __version__
ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
'TRIA_UP', # COMMITED
@ -52,8 +50,6 @@ def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=
From here:
https://gist.github.com/greenstick/b23e475d2bfdc3a82e34eaa1f6781ee4
"""
if total == 0:
return ""
filledLength = int(length * iteration // total)
bar = fill * filledLength + fill_empty * (length - filledLength)
return f"{prefix} |{bar}| {iteration}/{total}{suffix}"
@ -88,7 +84,7 @@ def get_state_str(state):
class SESSION_PT_settings(bpy.types.Panel):
"""Settings panel"""
bl_idname = "MULTIUSER_SETTINGS_PT_panel"
bl_label = " "
bl_label = ""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Multiuser"
@ -107,14 +103,14 @@ class SESSION_PT_settings(bpy.types.Panel):
layout.label(text=f"Session - {get_state_str(cli_state['STATE'])}", icon=connection_icon)
else:
layout.label(text=f"Session - v{__version__}",icon="PROP_OFF")
layout.label(text="Session",icon="PROP_OFF")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
row = layout.row()
runtime_settings = context.window_manager.session
settings = get_preferences()
settings = utils.get_preferences()
if hasattr(context.window_manager, 'session'):
# STATE INITIAL
@ -130,18 +126,14 @@ class SESSION_PT_settings(bpy.types.Panel):
current_state = cli_state['STATE']
# STATE ACTIVE
if current_state in [STATE_ACTIVE]:
if current_state in [STATE_ACTIVE, STATE_LOBBY]:
row.operator("session.stop", icon='QUIT', text="Exit")
row = layout.row()
if runtime_settings.is_host:
row = row.box()
row.label(text=f"LAN: {runtime_settings.internet_ip}", icon='INFO')
row.label(text=f"{runtime_settings.internet_ip}:{settings.port}", icon='INFO')
row = layout.row()
if current_state == STATE_LOBBY:
row = row.box()
row.label(text=f"Waiting the session to start", icon='INFO')
row = layout.row()
row.operator("session.stop", icon='QUIT', text="Exit")
# CONNECTION STATE
elif current_state in [STATE_SRV_SYNC,
STATE_SYNCING,
@ -197,7 +189,7 @@ class SESSION_PT_settings_network(bpy.types.Panel):
layout = self.layout
runtime_settings = context.window_manager.session
settings = get_preferences()
settings = utils.get_preferences()
# USER SETTINGS
row = layout.row()
@ -255,7 +247,7 @@ class SESSION_PT_settings_user(bpy.types.Panel):
layout = self.layout
runtime_settings = context.window_manager.session
settings = get_preferences()
settings = utils.get_preferences()
row = layout.row()
# USER SETTINGS
@ -286,88 +278,44 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
layout = self.layout
runtime_settings = context.window_manager.session
settings = get_preferences()
settings = utils.get_preferences()
net_section = layout.row().box()
net_section.prop(
settings,
"sidebar_advanced_net_expanded",
text="Network",
icon=get_expanded_icon(settings.sidebar_advanced_net_expanded),
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="")
net_section.label(text="Network ", icon='TRIA_DOWN')
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="")
replication_section = layout.row().box()
replication_section.prop(
settings,
"sidebar_advanced_rep_expanded",
text="Replication",
icon=get_expanded_icon(settings.sidebar_advanced_rep_expanded),
emboss=False)
if settings.sidebar_advanced_rep_expanded:
replication_section_row = replication_section.row()
replication_section_row.label(text="Sync flags", icon='COLLECTION_NEW')
replication_section_row = replication_section.row()
replication_section.label(text="Replication ", icon='TRIA_DOWN')
replication_section_row = replication_section.row()
if runtime_settings.session_mode == 'HOST':
replication_section_row.prop(settings.sync_flags, "sync_render_settings")
replication_section_row = replication_section.row()
replication_section_row.prop(settings, "enable_editmode_updates")
replication_section_row = replication_section.row()
if settings.enable_editmode_updates:
warning = replication_section_row.box()
warning.label(text="Don't use this with heavy meshes !", icon='ERROR')
replication_section_row = replication_section.row()
replication_section_row = replication_section.row()
replication_section_row.label(text="Per data type timers:")
replication_section_row = replication_section.row()
# Replication frequencies
flow = replication_section_row .grid_flow(
row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
line = flow.row(align=True)
line.label(text=" ")
line.separator()
line.label(text="refresh (sec)")
line.label(text="apply (sec)")
replication_section_row.label(text="Update method", icon='RECOVER_LAST')
replication_section_row = replication_section.row()
replication_section_row.prop(settings, "update_method", expand=True)
replication_section_row = replication_section.row()
replication_timers = replication_section_row.box()
replication_timers.label(text="Replication timers", icon='TIME')
if settings.update_method == "DEFAULT":
replication_timers = replication_timers.row()
# Replication frequencies
flow = replication_timers.grid_flow(
row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
line = flow.row(align=True)
line.label(text=" ")
line.separator()
line.label(text="refresh (sec)")
line.label(text="apply (sec)")
for item in settings.supported_datablocks:
line = flow.row(align=True)
line.prop(item, "auto_push", text="", icon=item.icon)
line.separator()
line.prop(item, "bl_delay_refresh", text="")
line.prop(item, "bl_delay_apply", text="")
for item in settings.supported_datablocks:
line = flow.row(align=True)
line.prop(item, "auto_push", text="", icon=item.icon)
line.separator()
line.prop(item, "bl_delay_refresh", text="")
line.prop(item, "bl_delay_apply", text="")
else:
replication_timers = replication_timers.row()
replication_timers.label(text="Update rate (ms):")
replication_timers.prop(settings, "depsgraph_update_rate", text="")
log_section = layout.row().box()
log_section.prop(
settings,
"sidebar_advanced_log_expanded",
text="Logging",
icon=get_expanded_icon(settings.sidebar_advanced_log_expanded),
emboss=False)
if settings.sidebar_advanced_log_expanded:
log_section_row = log_section.row()
log_section_row.label(text="Log level:")
log_section_row.prop(settings, 'logging_level', text="")
class SESSION_PT_user(bpy.types.Panel):
bl_idname = "MULTIUSER_USER_PT_panel"
bl_label = "Online users"
@ -386,7 +334,7 @@ class SESSION_PT_user(bpy.types.Panel):
layout = self.layout
online_users = context.window_manager.online_users
selected_user = context.window_manager.user_index
settings = get_preferences()
settings = utils.get_preferences()
active_user = online_users[selected_user] if len(
online_users)-1 >= selected_user else 0
runtime_settings = context.window_manager.session
@ -408,19 +356,17 @@ 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 operators.client.state['STATE'] == STATE_ACTIVE:
user_operations.alert = context.window_manager.session.time_snap_running
user_operations.operator(
"session.snapview",
text="",
icon='VIEW_CAMERA').target_client = active_user.username
user_operations.alert = context.window_manager.session.time_snap_running
user_operations.operator(
"session.snapview",
text="",
icon='VIEW_CAMERA').target_client = active_user.username
user_operations.alert = context.window_manager.session.user_snap_running
user_operations.operator(
"session.snaptime",
text="",
icon='TIME').target_client = active_user.username
user_operations.alert = context.window_manager.session.user_snap_running
user_operations.operator(
"session.snaptime",
text="",
icon='TIME').target_client = active_user.username
if operators.client.online_users[settings.username]['admin']:
user_operations.operator(
@ -432,7 +378,7 @@ class SESSION_PT_user(bpy.types.Panel):
class SESSION_UL_users(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
session = operators.client
settings = get_preferences()
settings = utils.get_preferences()
is_local_user = item.username == settings.username
ping = '-'
frame_current = '-'
@ -444,8 +390,8 @@ class SESSION_UL_users(bpy.types.UIList):
ping = str(user['latency'])
metadata = user.get('metadata')
if metadata and 'frame_current' in metadata:
frame_current = str(metadata.get('frame_current','-'))
scene_current = metadata.get('scene_current','-')
frame_current = str(metadata['frame_current'])
scene_current = metadata['scene_current']
if user['admin']:
status_icon = 'FAKE_USER_ON'
split = layout.split(factor=0.35)
@ -516,7 +462,7 @@ class SESSION_PT_services(bpy.types.Panel):
def draw_property(context, parent, property_uuid, level=0):
settings = get_preferences()
settings = utils.get_preferences()
runtime_settings = context.window_manager.session
item = operators.client.get(uuid=property_uuid)
@ -586,18 +532,9 @@ class SESSION_PT_repository(bpy.types.Panel):
@classmethod
def poll(cls, context):
session = operators.client
settings = get_preferences()
admin = False
if session and hasattr(session,'online_users'):
usr = session.online_users.get(settings.username)
if usr:
admin = usr['admin']
return hasattr(context.window_manager, 'session') and \
operators.client and \
(operators.client.state['STATE'] == STATE_ACTIVE or \
operators.client.state['STATE'] == STATE_LOBBY and admin)
operators.client.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]
def draw_header(self, context):
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
@ -606,7 +543,7 @@ class SESSION_PT_repository(bpy.types.Panel):
layout = self.layout
# Filters
settings = get_preferences()
settings = utils.get_preferences()
runtime_settings = context.window_manager.session
session = operators.client

View File

@ -39,7 +39,7 @@ def find_from_attr(attr_name, attr_value, list):
def get_datablock_users(datablock):
users = []
supported_types = get_preferences().supported_datablocks
supported_types = get_preferences().supported_datablocks
if hasattr(datablock, 'users_collection') and datablock.users_collection:
users.extend(list(datablock.users_collection))
if hasattr(datablock, 'users_scene') and datablock.users_scene:
@ -77,29 +77,10 @@ def resolve_from_id(id, optionnal_type=None):
if id in root and ((optionnal_type is None) or (optionnal_type.lower() in root[id].__class__.__name__.lower())):
return root[id]
return None
def get_datablock_from_uuid(uuid, default, ignore=[]):
if not uuid:
return default
for category in dir(bpy.data):
root = getattr(bpy.data, category)
if isinstance(root, Iterable) and category not in ignore:
for item in root:
if getattr(item, 'uuid', None) == uuid:
return item
return default
def get_preferences():
return bpy.context.preferences.addons[__package__].preferences
def current_milli_time():
return int(round(time.time() * 1000))
def get_expanded_icon(prop: bpy.types.BoolProperty) -> str:
if prop:
return 'DISCLOSURE_TRI_DOWN'
else:
return 'DISCLOSURE_TRI_RIGHT'
return int(round(time.time() * 1000))

View File

@ -2,7 +2,7 @@ import os
import pytest
from deepdiff import DeepDiff
from uuid import uuid4
import bpy
import random
from multi_user.bl_types.bl_collection import BlCollection
@ -10,13 +10,8 @@ from multi_user.bl_types.bl_collection import BlCollection
def test_collection(clear_blend):
# Generate a collection with childrens and a cube
datablock = bpy.data.collections.new("root")
datablock.uuid = str(uuid4())
s1 = bpy.data.collections.new("child")
s1.uuid = str(uuid4())
s2 = bpy.data.collections.new("child2")
s2.uuid = str(uuid4())
datablock.children.link(s1)
datablock.children.link(s2)
datablock.children.link(bpy.data.collections.new("child"))
datablock.children.link(bpy.data.collections.new("child2"))
bpy.ops.mesh.primitive_cube_add()
datablock.objects.link(bpy.data.objects[0])

View File

@ -30,11 +30,9 @@ CONSTRAINTS_TYPES = [
'COPY_ROTATION', 'COPY_SCALE', 'COPY_TRANSFORMS', 'LIMIT_DISTANCE',
'LIMIT_LOCATION', 'LIMIT_ROTATION', 'LIMIT_SCALE', 'MAINTAIN_VOLUME',
'TRANSFORM', 'TRANSFORM_CACHE', 'CLAMP_TO', 'DAMPED_TRACK', 'IK',
'LOCKED_TRACK', 'STRETCH_TO', 'TRACK_TO', 'ACTION',
'LOCKED_TRACK', 'SPLINE_IK', 'STRETCH_TO', 'TRACK_TO', 'ACTION',
'ARMATURE', 'CHILD_OF', 'FLOOR', 'FOLLOW_PATH', 'PIVOT', 'SHRINKWRAP']
#temporary disabled 'SPLINE_IK' until its fixed
def test_object(clear_blend):
bpy.ops.mesh.primitive_cube_add(
enter_editmode=False, align='WORLD', location=(0, 0, 0))