Merge branch 'develop' into 'master'

v0.0.2

Closes #69

See merge request slumber/multi-user!19
This commit is contained in:
Swann Martinez
2020-02-28 10:39:22 +00:00
60 changed files with 2019 additions and 777 deletions

3
.gitignore vendored
View File

@ -7,3 +7,6 @@ __pycache__/
cache cache
config config
*.code-workspace *.code-workspace
# sphinx build folder
_build

2
.gitmodules vendored
View File

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

38
CHANGELOG.md Normal file
View File

@ -0,0 +1,38 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.0.2] - 2020-02-28
### Added
- Blender animation features support (alpha).
- Action.
- Armature (Unstable).
- Shape key.
- Drivers.
- Constraints.
- Snap to user timeline tool.
- Light probes support (only since 2.83).
- Metaballs support.
- Improved modifiers support.
- Online documentation.
- Improved Undo handling.
- Improved overall session handling:
- Time To Leave : ensure clients/server disconnect automatically on connection lost.
- Ping: show clients latency.
- Non-blocking connection.
- Connection state tracking.
- Service communication layer to manage background daemons.
### Changed
- UI revamp:
- Show users frame.
- Expose IPC(inter process communication) port.
- New user list.
- Progress bar to track connection status.
- Right management takes view-layer in account for object selection.
- Use a basic BFS approach for replication graph pre-load.
- Serialization is now based on marshal (2x performance improvements).
- Let pip chose python dependencies install path.

View File

@ -9,7 +9,7 @@
This tool aims to allow multiple users to work on the same scene over the network. Based on a Clients / Server architecture, the data-oriented replication schema replicate blender data-blocks across the wire. This tool aims to allow multiple users to work on the same scene over the network. Based on a Clients / Server architecture, the data-oriented replication schema replicate blender data-blocks across the wire.
## Installation ## Quick installation
1. Download latest release [multi_user.zip](/uploads/8aef79c7cf5b1d9606dc58307fd9ad8b/multi_user.zip). 1. Download latest release [multi_user.zip](/uploads/8aef79c7cf5b1d9606dc58307fd9ad8b/multi_user.zip).
2. Run blender as administrator (dependencies installation). 2. Run blender as administrator (dependencies installation).
@ -19,16 +19,16 @@ This tool aims to allow multiple users to work on the same scene over the networ
## Usage ## Usage
See [how to](https://gitlab.com/slumber/multi-user/wikis/User/Quickstart) section. See the [documentation](https://multi-user.readthedocs.io/en/latest/) for details.
## Current development status ## Current development status
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones. Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
| Name | Status | Comment | | Name | Status | Comment |
| ---------- | :----------------: | :------------: | | ----------- | :----------------: | :------------: |
| action | :x: | WIP | | action | :exclamation: | Not stable |
| armature | :x: | WIP | | armature | :exclamation: | Not stable |
| camera | :white_check_mark: | | | camera | :white_check_mark: | |
| collection | :white_check_mark: | | | collection | :white_check_mark: | |
| curve | :white_check_mark: | Not tested | | curve | :white_check_mark: | Not tested |
@ -36,14 +36,15 @@ Currently, not all data-block are supported for replication over the wire. The f
| image | :exclamation: | Not stable yet | | image | :exclamation: | Not stable yet |
| mesh | :white_check_mark: | | | mesh | :white_check_mark: | |
| material | :white_check_mark: | | | material | :white_check_mark: | |
| metaball | :x: | | | metaball | :white_check_mark: | |
| object | :white_check_mark: | | | object | :white_check_mark: | |
| scene | :white_check_mark: | | | scene | :white_check_mark: | |
| world | :white_check_mark: | | | world | :white_check_mark: | |
| lightprobes | :white_check_mark: | |
### Performance issues ### Performance issues
Since this addon is written in pure python for a prototyping purpose, performances could be better from all perspective. Since this addon is written in pure python for a research purpose, performances could be better from all perspective.
I'm working on it. I'm working on it.
## Dependencies ## Dependencies
@ -58,12 +59,10 @@ I'm working on it.
## Contributing ## Contributing
1. Fork it (<https://gitlab.com/yourname/yourproject/fork>) See [contributing section](https://multi-user.readthedocs.io/en/latest/ways_to_contribute.html) of the documentation.
2. Create your feature branch (`git checkout -b feature/fooBar`)
3. Commit your changes (`git commit -am 'Add some fooBar'`)
4. Push to the branch (`git push origin feature/fooBar`)
5. Create a new Pull Request
## Licensing ## Licensing
See [license](LICENSE) See [license](LICENSE)
[![Documentation Status](https://readthedocs.org/projects/multi-user/badge/?version=latest)](https://multi-user.readthedocs.io/en/latest/?badge=latest)

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

8
docs/about/index.rst Normal file
View File

@ -0,0 +1,8 @@
About
=====
.. toctree::
:maxdepth: 1
:name: toc-about
introduction

View File

@ -0,0 +1,10 @@
============
Introduction
============
A film is an idea carved along the whole production process by many different peoples. A traditional animation pipeline involve a linear succession of tasks. From storyboard to compositing by passing upon different step, its fundamental work flow is similar to an industrial assembly line. Since each step is almost a department, its common that one person on department B doesn't know what another person did on a previous step in a department A. This lack of visibility/communication could be a source of problems which could produce a bad impact on the final production result.
Nowadays it's a known fact that real-time rendering technologies allows to speedup traditional linear production by reducing drastically the iteration time across different steps. All majors industrial CG solutions are moving toward real-time horizons to bring innovative interactive workflows. But this is a microscopic, per-task/solution vision of real-time rendering benefits for the animation production. What if we step-back, get a macroscopic picture of an animation movie pipeline and ask ourself how real-time could change our global workflow ? Could-it bring better ways of working together by giving more visibility between departments during the whole production ?
The multi-user addon is an attempt to experiment real-time parallelism between different production stage. By replicating blender data blocks over the networks, it allows different artists to collaborate on a same scene in real-time.

98
docs/conf.py Normal file
View File

@ -0,0 +1,98 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'multi-user'
copyright = '2020, Swann Martinez'
author = 'Swann Martinez'
# The full version, including alpha/beta/rc tags
release = '0.0.1'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'python'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
if on_rtd:
using_rtd_theme = True
# Theme options
html_theme_options = {
# 'typekit_id': 'hiw1hhg',
# 'analytics_id': '',
# 'sticky_navigation': True # Set to False to disable the sticky nav while scrolling.
# 'logo_only': True, # if we have a html_logo below, this shows /only/ the logo with no title text
'collapse_navigation': False, # Collapse navigation (False makes it tree-like)
# 'display_version': True, # Display the docs version
# 'navigation_depth': 4, # Depth of the headers shown in the navigation bar
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'multiusrdoc'
# sphinx-notfound-page
# https://github.com/readthedocs/sphinx-notfound-page
notfound_context = {
'title': 'Page Not Found',
'body': '''
<h1>Page Not Found</h1>
<p>Sorry, we couldn't find that page.</p>
<p>Try using the search box or go to the homepage.</p>
''',
}
# Enable directives that insert the contents of external files
file_insertion_enabled = False

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,9 @@
Getting started
===============
.. toctree::
:maxdepth: 1
:name: toc-getting-started
install
quickstart

View File

@ -0,0 +1,9 @@
============
Installation
============
*The process is the same for linux, mac and windows.*
1. Download latest release `multi_user.zip <https://gitlab.com/slumber/multi-user/uploads/8aef79c7cf5b1d9606dc58307fd9ad8b/multi_user.zip>`_.
2. Run blender as administrator (to allow python dependencies auto-installation).
3. Install last_version.zip from your addon preferences.

View File

@ -0,0 +1,111 @@
===========
Quick start
===========
*All settings are located under: `View3D -> Sidebar -> Multiuser panel`*
Session setup
=============
This section describe how to create or join a collaborative session.
---------------------
1. User information's
---------------------
.. image:: img/quickstart_user_infos.png
- **name**: username.
- **color**: color used to represent the user into other user workspace.
----------
2. Network
----------
.. note:: If you host a session over internet, special network configuration is needed.
Hosting and connection are done from this panel.
+-----------------------------------+-------------------------------------+
| Host | Join |
+===================================+=====================================+
|.. image:: img/quickstart_host.png | .. image:: img/quickstart_join.png |
+-----------------------------------+-------------------------------------+
| | Start empty: Cleanup the file | | IP: server ip |
| | before hosting | | Port: server port |
+-----------------------------------+-------------------------------------+
| **HOST**: Host a session | **CONNECT**: Join a session |
+-----------------------------------+-------------------------------------+
**Port configuration:**
For now, a session use 4 ports to run.
If 5555 is given in host settings, it will use 5555, 5556 (5555+1), 5557 (5555+2), 5558 (5555+3).
------------
2.1 Advanced
------------
.. image:: img/quickstart_advanced.png
**Right strategy** (only host) enable you to choose between a strict and a relaxed pattern:
- **Strict**: Host is the king, by default the host own each properties, only him can grant modification rights.
- **Common**: Each properties are under common rights by default, on selection, a property is only modifiable by the owner.
On each strategy, when a user is the owner he can choose to pass his rights to someone else.
**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)
.. note:: Per-data type settings will soon be revamped for simplification purposes
Session Management
==================
This section describe tools available during a collaborative session.
---------------
Connected users
---------------
.. image:: img/quickstart_users.png
This panel displays all connected users information's, including yours.
By selecting a user in the list you'll have access to different **actions**:
- The **camera button** allow you to snap on the user viewpoint.
- The **time button** allow you to snap on the user time.
---------------------
Replicated properties
---------------------
.. image:: img/quickstart_properties.png
The **replicated properties** panel shows all replicated properties status and associated actions.
Since the replication architecture is based on commit/push/pull mechanisms, a replicated properties can be pushed/pull or even committed manually from this panel.
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
| icon | Action | Description |
+=======================================+===================+====================================================================================+
| .. image:: img/quickstart_push.png | **Push** | push data-block to other clients |
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
| .. image:: img/quickstart_pull.png | **Pull** | pull last version into blender |
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
| .. image:: img/quickstart_refresh.png | **Reset** | Reset local change to the server version |
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
| .. image:: img/quickstart_unlock.png | **Lock/Unlock** | If locked, does nothing. If unlocked, grant modification rights to another user. |
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
| .. image:: img/quickstart_remove.png | **Delete** | Remove the data-block from network replication |
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+

BIN
docs/img/homepage_ban.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

59
docs/index.rst Normal file
View File

@ -0,0 +1,59 @@
=====================================
Welcome to Multi-user's documentation
=====================================
.. image:: img/homepage_ban.png
The multi-user addon is a free and open source blender plugin. It tool aims to bring multiple users to work on the same .blend over the network.
.. warning:: Under development, use it at your own risks.
Main Features
=============
- Collaborative workflow in blender
- Viewport users presence (active selection, POV)
- Datablocks right managment
- Tested under Windows
Status
======
.. image:: img/homepage_roadmap.png
Follow the `roadmap <https://gitlab.com/slumber/multi-user/-/boards/929107>`_ to be aware of last news.
Documentation is organized into the following sections:
.. toctree::
:maxdepth: 1
:caption: About
:name: sec-about
about/introduction
.. toctree::
:maxdepth: 1
:caption: Getting started
:name: sec-learn
getting_started/install
getting_started/quickstart
.. toctree::
:maxdepth: 1
:caption: Tutorials
:name: sec-tutorials
tutorials/hosting_guide
.. toctree::
:maxdepth: 1
:caption: Community
:name: sec-community
ways_to_contribute

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -0,0 +1,47 @@
================
Advanced hosting
================
This tutorial aims to guide you to host a collaborative Session on internet.
.. note::
This tutorial will change soon with the new dedicated server.
The multi-user network architecture is based on a clients-server model. The communication protocol use four ports to communicate with client:
* Commands: command transmission (such as **snapshots**, **change_rights**, etc.)
* Subscriber: pull data
* Publisher: push data
* TTL (time to leave): used to ping each clients
.. warning::
Until now, those communications are not encrypted but are planned to be in a mid-term future (`Status <https://gitlab.com/slumber/multi-user/issues/62>`_).
To know which ports will be used, you just have to read the port in your preference.
.. image:: img/hosting_guide_port.png
:align: center
:alt: Port
In the picture below we have setup our port to **5555** so it will be:
* Commands: 5555 (**5555** +0)
* Subscriber: 5556 (**5555** +1)
* Publisher: 5557 (**5555** +2)
* TTL: 5558 (**5555** +3)
Now that we know which port are needed to communicate we need to allow other computer to communicate with our one.
By default your router shall block those ports. In order grant server access to people from internet you have multiple options:
1. Simple: use a third party software like `HAMACHI <https://vpn.net/>`_ (Free until 5 users) or `ZEROTIER <https://www.zerotier.com/download/>`_ to handle network sharing.
2. Harder: Setup a VPN server and allow distant user to connect to your VPN.
3. **Not secure** but simple: Setup port forwarding for each ports (for example 5555,5556,5557 and 5558 in our case). You can follow this `guide <https://www.wikihow.com/Set-Up-Port-Forwarding-on-a-Router>`_ for example.
Once you have setup the network, you can run **HOST** in order to start the server. Then other users could join your session in the regular way.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

8
docs/tutorials/index.rst Normal file
View File

@ -0,0 +1,8 @@
Tutorials
=========
.. toctree::
:maxdepth: 1
:name: toc-tutorial
hosting_guide

View File

@ -0,0 +1,42 @@
==================
Ways to contribute
==================
.. Note:: Work in progress
Testing and reporting issues
============================
A great way of contributing to the multi-user addon is to test development branch and to report issues.
It is also helpful to report issues discovered in releases, so that they can be fixed in the development branch and in future releases.
----------------------------
Testing development versions
----------------------------
In order to help with the testing, you have several possibilities:
- Test `latest release <https://gitlab.com/slumber/multi-user/-/tags>`_
- Test `development branch <https://gitlab.com/slumber/multi-user/-/branches>`_
--------------------------
Filling an issue on Gitlab
--------------------------
The `gitlab issue tracker <https://gitlab.com/slumber/multi-user/issues>`_ is used for bug report and enhancement suggestion.
You will need a Gitlab account to be able to open a new issue there and click on "New issue" button.
Here are some useful information you should provide in a bug report:
- **Multi-user version** such as *lastest*, *commit-hash*, *branch*. This is a must have. Some issues might be relevant in the current stable release, but fixed in the development branch.
- **How to reproduce the bug**. In the majority of cases, bugs are reproducible, i.e. it is possible to trigger them reliably by following some steps. Please always describe those steps as clearly as possible, so that everyone can try to reproduce the issue and confirm it. It could also take the form of a screen capture.
Contributing code
=================
1. Fork it (https://gitlab.com/yourname/yourproject/fork)
2. Create your feature branch (git checkout -b feature/fooBar)
3. Commit your changes (git commit -am 'Add some fooBar')
4. Push to the branch (git push origin feature/fooBar)
5. Create a new Pull Request

View File

@ -1,11 +1,15 @@
bl_info = { bl_info = {
"name": "Multi-User", "name": "Multi-User",
"author": "Swann Martinez", "author": "Swann Martinez",
"description": "", "version": (0, 0, 2),
"description": "Enable real-time collaborative workflow inside blender",
"blender": (2, 80, 0), "blender": (2, 80, 0),
"location": "", "location": "3D View > Sidebar > Multi-User tab",
"warning": "Unstable addon, use it at your own risks", "warning": "Unstable addon, use it at your own risks",
"category": "Collaboration" "category": "Collaboration",
"wiki_url": "https://multi-user.readthedocs.io/en/develop/index.html",
"tracker_url": "https://gitlab.com/slumber/multi-user/issues",
"support": "COMMUNITY"
} }
@ -31,45 +35,45 @@ DEPENDENCIES = {
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR) logger.setLevel(logging.WARNING)
#TODO: refactor config #TODO: refactor config
# UTILITY FUNCTIONS # UTILITY FUNCTIONS
def generate_supported_types(): def generate_supported_types():
stype_dict = {'supported_types':{}} stype_dict = {'supported_types':{}}
for type in bl_types.types_to_register(): for type in bl_types.types_to_register():
_type = getattr(bl_types, type) type_module = getattr(bl_types, type)
props = {} type_impl_name = "Bl{}".format(type.split('_')[1].capitalize())
props['bl_delay_refresh']=_type.bl_delay_refresh type_module_class = getattr(type_module, type_impl_name)
props['bl_delay_apply']=_type.bl_delay_apply
props['use_as_filter'] = False
props['icon'] = _type.bl_icon
props['auto_push']=_type.bl_automatic_push
props['bl_name']=_type.bl_id
stype_dict['supported_types'][_type.bl_rep_class.__name__] = props props = {}
props['bl_delay_refresh']=type_module_class.bl_delay_refresh
props['bl_delay_apply']=type_module_class.bl_delay_apply
props['use_as_filter'] = False
props['icon'] = type_module_class.bl_icon
props['auto_push']=type_module_class.bl_automatic_push
props['bl_name']=type_module_class.bl_id
stype_dict['supported_types'][type_impl_name] = props
return stype_dict return stype_dict
def client_list_callback(scene, context): def client_list_callback(scene, context):
from . import operators from . import operators
from .bl_types.bl_user import BlUser
items = [(RP_COMMON, RP_COMMON, "")] items = [(RP_COMMON, RP_COMMON, "")]
username = bpy.context.window_manager.session.username username = bpy.context.window_manager.session.username
cli = operators.client cli = operators.client
if cli: if cli:
client_keys = cli.list(filter=BlUser) client_ids = cli.online_users.keys()
for k in client_keys: for id in client_ids:
name = cli.get(uuid=k).data["name"] name_desc = id
if id == username:
name_desc = name
if name == username:
name_desc += " (self)" name_desc += " (self)"
items.append((name, name_desc, "")) items.append((id, name_desc, ""))
return items return items
@ -90,6 +94,15 @@ class ReplicatedDatablock(bpy.types.PropertyGroup):
auto_push: bpy.props.BoolProperty(default=True) auto_push: bpy.props.BoolProperty(default=True)
icon: bpy.props.StringProperty() icon: bpy.props.StringProperty()
class SessionUser(bpy.types.PropertyGroup):
"""Session User
Blender user information property
"""
username: bpy.props.StringProperty(name="username")
current_frame: bpy.props.IntProperty(name="current_frame")
class SessionProps(bpy.types.PropertyGroup): class SessionProps(bpy.types.PropertyGroup):
username: bpy.props.StringProperty( username: bpy.props.StringProperty(
name="Username", name="Username",
@ -109,25 +122,19 @@ class SessionProps(bpy.types.PropertyGroup):
description='Distant host port', description='Distant host port',
default=5555 default=5555
) )
add_property_depth: bpy.props.IntProperty( ipc_port: bpy.props.IntProperty(
name="add_property_depth", name="ipc_port",
default=1 description='internal ttl port(only usefull for multiple local instances)',
default=5561
) )
outliner_filter: bpy.props.StringProperty(name="None")
is_admin: bpy.props.BoolProperty( is_admin: bpy.props.BoolProperty(
name="is_admin", name="is_admin",
default=False default=False
) )
init_scene: bpy.props.BoolProperty(
name="init_scene",
default=True
)
start_empty: bpy.props.BoolProperty( start_empty: bpy.props.BoolProperty(
name="start_empty", name="start_empty",
default=True default=True
) )
active_object: bpy.props.PointerProperty(
name="active_object", type=bpy.types.Object)
session_mode: bpy.props.EnumProperty( session_mode: bpy.props.EnumProperty(
name='session_mode', name='session_mode',
description='session mode', description='session mode',
@ -179,10 +186,11 @@ class SessionProps(bpy.types.PropertyGroup):
description='Show only owned datablocks', description='Show only owned datablocks',
default=True default=True
) )
use_select_right: bpy.props.BoolProperty( user_snap_running: bpy.props.BoolProperty(
name="Selection right", default=False
description='Change right on selection', )
default=True time_snap_running: bpy.props.BoolProperty(
default=False
) )
def load(self): def load(self):
@ -239,12 +247,13 @@ class SessionProps(bpy.types.PropertyGroup):
classes = ( classes = (
SessionUser,
ReplicatedDatablock, ReplicatedDatablock,
SessionProps, SessionProps,
) )
libs = os.path.dirname(os.path.abspath(__file__))+"\\libs\\replication" libs = os.path.dirname(os.path.abspath(__file__))+"\\libs\\replication\\replication"
@persistent @persistent
def load_handler(dummy): def load_handler(dummy):
@ -267,7 +276,10 @@ def register():
bpy.types.WindowManager.session = bpy.props.PointerProperty( bpy.types.WindowManager.session = bpy.props.PointerProperty(
type=SessionProps) type=SessionProps)
bpy.types.ID.uuid = bpy.props.StringProperty(default="") bpy.types.ID.uuid = bpy.props.StringProperty(default="")
bpy.types.WindowManager.online_users = bpy.props.CollectionProperty(
type=SessionUser
)
bpy.types.WindowManager.user_index = bpy.props.IntProperty()
bpy.context.window_manager.session.load() bpy.context.window_manager.session.load()
presence.register() presence.register()

View File

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

View File

@ -1,5 +1,6 @@
import bpy import bpy
import mathutils import mathutils
import copy
from .. import utils from .. import utils
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
@ -7,32 +8,109 @@ from .bl_datablock import BlDatablock
# WIP # WIP
class BlAction(BlDatablock): class BlAction(BlDatablock):
def load(self, data, target): bl_id = "actions"
utils.dump_anything.load(target, data) bl_class = bpy.types.Action
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'ACTION_TWEAK'
def construct(self, data): def construct(self, data):
return bpy.data.actions.new(data["name"]) return bpy.data.actions.new(data["name"])
def load(self, data, target): def load(self, data, target):
pass begin_frame = 100000
# # find target object end_frame = -100000
# object_ = bpy.context.scene.objects.active
# if object_ is None: for dumped_fcurve in data["fcurves"]:
# raise RuntimeError("Nothing is selected.") begin_frame = min(
# if object_.mode != 'POSE': # object must be in pose mode begin_frame,
# raise RuntimeError("Object must be in pose mode.") min(
# if object_.animation_data.action is None: [begin_frame] + [dkp["co"][0] for dkp in dumped_fcurve["keyframe_points"]]
# raise RuntimeError("Object needs an active action.") )
)
end_frame = max(
end_frame,
max(
[end_frame] + [dkp["co"][0] for dkp in dumped_fcurve["keyframe_points"]]
)
)
begin_frame = 0
loader = utils.dump_anything.Loader()
for dumped_fcurve in data["fcurves"]:
dumped_data_path = dumped_fcurve["data_path"]
dumped_array_index = dumped_fcurve["dumped_array_index"]
# create fcurve if needed
fcurve = target.fcurves.find(dumped_data_path, index=dumped_array_index)
if fcurve is None:
fcurve = target.fcurves.new(dumped_data_path, index=dumped_array_index)
# remove keyframes within dumped_action range
for keyframe in reversed(fcurve.keyframe_points):
if end_frame >= (keyframe.co[0] + begin_frame ) >= begin_frame:
fcurve.keyframe_points.remove(keyframe, fast=True)
# paste dumped keyframes
for dumped_keyframe_point in dumped_fcurve["keyframe_points"]:
if dumped_keyframe_point['type'] == '':
dumped_keyframe_point['type'] = 'KEYFRAME'
new_kf = fcurve.keyframe_points.insert(
dumped_keyframe_point["co"][0] - begin_frame,
dumped_keyframe_point["co"][1],
options={'FAST', 'REPLACE'}
)
keycache = copy.copy(dumped_keyframe_point)
keycache = utils.dump_anything.remove_items_from_dict(
keycache,
["co", "handle_left", "handle_right",'type']
)
loader.load(
new_kf,
keycache
)
new_kf.type = dumped_keyframe_point['type']
new_kf.handle_left = [
dumped_keyframe_point["handle_left"][0] - begin_frame,
dumped_keyframe_point["handle_left"][1]
]
new_kf.handle_right = [
dumped_keyframe_point["handle_right"][0] - begin_frame,
dumped_keyframe_point["handle_right"][1]
]
# clearing (needed for blender to update well)
if len(fcurve.keyframe_points) == 0:
target.fcurves.remove(fcurve)
target.id_root= data['id_root']
def dump(self, pointer=None): def dump(self, pointer=None):
assert(pointer) assert(pointer)
data = utils.dump_datablock(pointer, 1)
dumper = utils.dump_anything.Dumper() dumper = utils.dump_anything.Dumper()
dumper.depth = 2 dumper.exclude_filter =[
'name_full',
'original',
'use_fake_user',
'user',
'is_library_indirect',
'select_control_point',
'select_right_handle',
'select_left_handle',
'uuid',
'users'
]
dumper.depth = 1
data = dumper.dump(pointer)
data["fcurves"] = [] data["fcurves"] = []
dumper.depth = 2
for fcurve in self.pointer.fcurves: for fcurve in self.pointer.fcurves:
fc = { fc = {
"data_path": fcurve.data_path, "data_path": fcurve.data_path,
@ -49,20 +127,7 @@ class BlAction(BlDatablock):
return data return data
def resolve(self):
assert(self.data)
self.pointer = bpy.data.actions.get(self.data['name'])
def diff(self):
return False
def is_valid(self): def is_valid(self):
return bpy.data.actions.get(self.data['name']) return bpy.data.actions.get(self.data['name'])
bl_id = "actions"
bl_class = bpy.types.Action
bl_rep_class = BlAction
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'ACTION_TWEAK'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,8 +15,11 @@ def dump_image(image):
image.save() image.save()
if image.source == "FILE": 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() image.save()
file = open(image.filepath_raw, "rb") file = open(image_path, "rb")
pixels = file.read() pixels = file.read()
file.close() file.close()
else: else:
@ -24,6 +27,13 @@ def dump_image(image):
return pixels return pixels
class BlImage(BlDatablock): class BlImage(BlDatablock):
bl_id = "images"
bl_class = bpy.types.Image
bl_delay_refresh = 0
bl_delay_apply = 1
bl_automatic_push = False
bl_icon = 'IMAGE_DATA'
def construct(self, data): def construct(self, data):
return bpy.data.images.new( return bpy.data.images.new(
name=data['name'], name=data['name'],
@ -44,32 +54,30 @@ class BlImage(BlDatablock):
image.source = 'FILE' image.source = 'FILE'
image.filepath = img_path image.filepath = img_path
image.colorspace_settings.name = data["colorspace_settings"]["name"]
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
data = {} data = {}
data['pixels'] = dump_image(pointer) data['pixels'] = dump_image(pointer)
utils.dump_datablock_attibutes(pointer, [], 2, data) dumper = utils.dump_anything.Dumper()
data = utils.dump_datablock_attibutes( dumper.depth = 2
pointer, dumper.include_filter = [
["name", 'size', 'height', 'alpha', 'float_buffer', 'filepath', 'source'], "name",
2, 'size',
data) 'height',
return data 'alpha',
'float_buffer',
'filepath',
'source',
'colorspace_settings']
data.update(dumper.dump(pointer))
def resolve(self): return data
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.images)
def diff(self): def diff(self):
return False return False
def is_valid(self): def is_valid(self):
return bpy.data.images.get(self.data['name']) return bpy.data.images.get(self.data['name'])
bl_id = "images"
bl_class = bpy.types.Image
bl_rep_class = BlImage
bl_delay_refresh = 0
bl_delay_apply = 0
bl_automatic_push = False
bl_icon = 'IMAGE_DATA'

View File

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

View File

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

View File

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

View File

@ -1,24 +1,44 @@
import bpy import bpy
import mathutils import mathutils
import logging
from .. import utils from .. import utils
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
logger = logging.getLogger(__name__)
class BlLightprobe(BlDatablock):
bl_id = "lightprobes"
bl_class = bpy.types.LightProbe
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LIGHTPROBE_GRID'
class BlLightProbe(BlDatablock):
def load(self, data, target): def load(self, data, target):
utils.dump_anything.load(target, data) utils.dump_anything.load(target, data)
def construct(self, data): def construct(self, data):
return bpy.data.lightprobes.new(data["name"]) type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type']
# See https://developer.blender.org/D6396
if bpy.app.version[1] >= 83:
return bpy.data.lightprobes.new(data["name"], type)
else:
logger.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
def dump(self, pointer=None): def dump(self, pointer=None):
assert(pointer) assert(pointer)
if bpy.app.version[1] < 83:
logger.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
dumper = utils.dump_anything.Dumper() dumper = utils.dump_anything.Dumper()
dumper.depth = 1 dumper.depth = 1
dumper.include_filter = [ dumper.include_filter = [
"name", "name",
'type',
'influence_type', 'influence_type',
'influence_distance', 'influence_distance',
'falloff', 'falloff',
@ -29,21 +49,15 @@ class BlLightProbe(BlDatablock):
'use_custom_parallax', 'use_custom_parallax',
'parallax_type', 'parallax_type',
'parallax_distance', 'parallax_distance',
'grid_resolution_x',
'grid_resolution_y',
'grid_resolution_z',
'visibility_buffer_bias',
'visibility_bleed_bias',
'visibility_blur'
] ]
return dumper.dump(pointer) return dumper.dump(pointer)
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lattices)
def is_valid(self): def is_valid(self):
return bpy.data.lattices.get(self.data['name']) return bpy.data.lattices.get(self.data['name'])
bl_id = "lightprobes"
bl_class = bpy.types.LightProbe
bl_rep_class = BlLightProbe
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'LIGHTPROBE_GRID'

View File

@ -68,10 +68,17 @@ def load_link(target_node_tree, source):
class BlMaterial(BlDatablock): class BlMaterial(BlDatablock):
bl_id = "materials"
bl_class = bpy.types.Material
bl_delay_refresh = 10
bl_delay_apply = 10
bl_automatic_push = True
bl_icon = 'MATERIAL_DATA'
def construct(self, data): def construct(self, data):
return bpy.data.materials.new(data["name"]) return bpy.data.materials.new(data["name"])
def load(self, data, target): def load_implementation(self, data, target):
target.name = data['name'] target.name = data['name']
if data['is_grease_pencil']: if data['is_grease_pencil']:
if not target.is_grease_pencil: if not target.is_grease_pencil:
@ -88,6 +95,8 @@ class BlMaterial(BlDatablock):
target.node_tree.nodes.clear() target.node_tree.nodes.clear()
utils.dump_anything.load(target,data)
# Load nodes # Load nodes
for node in data["node_tree"]["nodes"]: for node in data["node_tree"]["nodes"]:
load_node(target.node_tree, data["node_tree"]["nodes"][node]) load_node(target.node_tree, data["node_tree"]["nodes"][node])
@ -98,7 +107,7 @@ class BlMaterial(BlDatablock):
for link in data["node_tree"]["links"]: for link in data["node_tree"]["links"]:
load_link(target.node_tree, data["node_tree"]["links"][link]) load_link(target.node_tree, data["node_tree"]["links"][link])
def dump(self, pointer=None): def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
mat_dumper = utils.dump_anything.Dumper() mat_dumper = utils.dump_anything.Dumper()
mat_dumper.depth = 2 mat_dumper.depth = 2
@ -115,6 +124,7 @@ class BlMaterial(BlDatablock):
node_dumper.depth = 1 node_dumper.depth = 1
node_dumper.exclude_filter = [ node_dumper.exclude_filter = [
"dimensions", "dimensions",
"show_expanded"
"select", "select",
"bl_height_min", "bl_height_min",
"bl_height_max", "bl_height_max",
@ -133,7 +143,12 @@ class BlMaterial(BlDatablock):
input_dumper.include_filter = ["default_value"] input_dumper.include_filter = ["default_value"]
links_dumper = utils.dump_anything.Dumper() links_dumper = utils.dump_anything.Dumper()
links_dumper.depth = 3 links_dumper.depth = 3
links_dumper.exclude_filter = ["dimensions"] links_dumper.include_filter = [
"name",
"to_node",
"from_node",
"from_socket",
"to_socket"]
data = mat_dumper.dump(pointer) data = mat_dumper.dump(pointer)
if pointer.use_nodes: if pointer.use_nodes:
@ -175,9 +190,6 @@ class BlMaterial(BlDatablock):
utils.dump_datablock_attibutes(pointer, ["grease_pencil"], 3, data) utils.dump_datablock_attibutes(pointer, ["grease_pencil"], 3, data)
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.materials)
def resolve_dependencies(self): def resolve_dependencies(self):
# TODO: resolve node group deps # TODO: resolve node group deps
deps = [] deps = []
@ -194,11 +206,3 @@ class BlMaterial(BlDatablock):
def is_valid(self): def is_valid(self):
return bpy.data.materials.get(self.data['name']) return bpy.data.materials.get(self.data['name'])
bl_id = "materials"
bl_class = bpy.types.Material
bl_rep_class = BlMaterial
bl_delay_refresh = 10
bl_delay_apply = 10
bl_automatic_push = True
bl_icon = 'MATERIAL_DATA'

View File

@ -3,14 +3,17 @@ import bmesh
import mathutils import mathutils
from .. import utils from .. import utils
from ..libs.replication.replication.constants import DIFF_BINARY
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
def dump_mesh(mesh, data={}): def dump_mesh(mesh, data={}):
import bmesh import bmesh
mesh_data = data mesh_data = data
mesh_buffer = bmesh.new() mesh_buffer = bmesh.new()
# https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html
mesh_buffer.from_mesh(mesh) mesh_buffer.from_mesh(mesh)
uv_layer = mesh_buffer.loops.layers.uv.verify() uv_layer = mesh_buffer.loops.layers.uv.verify()
@ -72,20 +75,25 @@ def dump_mesh(mesh, data={}):
uv_layers.append(uv_layer.name) uv_layers.append(uv_layer.name)
mesh_data["uv_layers"] = uv_layers mesh_data["uv_layers"] = uv_layers
return mesh_data # return mesh_data
class BlMesh(BlDatablock): class BlMesh(BlDatablock):
bl_id = "meshes"
bl_class = bpy.types.Mesh
bl_delay_refresh = 10
bl_delay_apply = 10
bl_automatic_push = True
bl_icon = 'MESH_DATA'
def construct(self, data): def construct(self, data):
instance = bpy.data.meshes.new(data["name"]) instance = bpy.data.meshes.new(data["name"])
instance.uuid = self.uuid instance.uuid = self.uuid
return instance return instance
def load(self, data, target): def load_implementation(self, data, target):
if not target or not target.is_editmode: if not target or not target.is_editmode:
# 1 - LOAD MATERIAL SLOTS # 1 - LOAD MATERIAL SLOTS
material_to_load = []
material_to_load = utils.revers(data["materials"])
target.materials.clear()
# SLots # SLots
i = 0 i = 0
@ -106,6 +114,8 @@ class BlMesh(BlDatablock):
v2 = data["edges"][i]["verts"][1] v2 = data["edges"][i]["verts"][1]
edge = mesh_buffer.edges.new([verts[v1], verts[v2]]) edge = mesh_buffer.edges.new([verts[v1], verts[v2]])
edge.smooth = data["edges"][i]["smooth"] edge.smooth = data["edges"][i]["smooth"]
mesh_buffer.edges.ensure_lookup_table()
for p in data["faces"]: for p in data["faces"]:
verts = [] verts = []
for v in data["faces"][p]["verts"]: for v in data["faces"][p]["verts"]:
@ -129,21 +139,25 @@ class BlMesh(BlDatablock):
# 3 - LOAD METADATA # 3 - LOAD METADATA
# uv's # uv's
for uv_layer in data['uv_layers']: utils.dump_anything.load(target.uv_layers, data['uv_layers'])
target.uv_layers.new(name=uv_layer)
bevel_layer = mesh_buffer.verts.layers.bevel_weight.verify() bevel_layer = mesh_buffer.verts.layers.bevel_weight.verify()
skin_layer = mesh_buffer.verts.layers.skin.verify() skin_layer = mesh_buffer.verts.layers.skin.verify()
utils.dump_anything.load(target, data) utils.dump_anything.load(target, data)
def dump_implementation(self, data, pointer=None):
def dump(self, pointer=None):
assert(pointer) assert(pointer)
data = utils.dump_datablock(pointer, 2) dumper = utils.dump_anything.Dumper()
data = dump_mesh(pointer, data) dumper.depth = 2
dumper.include_filter = [
'name',
'use_auto_smooth',
'auto_smooth_angle'
]
data = dumper.dump(pointer)
dump_mesh(pointer, data)
# Fix material index # Fix material index
m_list = [] m_list = []
for material in pointer.materials: for material in pointer.materials:
@ -154,9 +168,6 @@ class BlMesh(BlDatablock):
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.meshes)
def resolve_dependencies(self): def resolve_dependencies(self):
deps = [] deps = []
@ -168,11 +179,3 @@ class BlMesh(BlDatablock):
def is_valid(self): def is_valid(self):
return bpy.data.meshes.get(self.data['name']) return bpy.data.meshes.get(self.data['name'])
bl_id = "meshes"
bl_class = bpy.types.Mesh
bl_rep_class = BlMesh
bl_delay_refresh = 10
bl_delay_apply = 10
bl_automatic_push = True
bl_icon = 'MESH_DATA'

View File

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

View File

@ -1,11 +1,43 @@
import bpy import bpy
import mathutils import mathutils
import logging
from .. import utils from .. import utils
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
logger = logging.getLogger(__name__)
def load_constraints(target, data):
for local_constraint in target.constraints:
if local_constraint.name not in data:
target.constraints.remove(local_constraint)
for constraint in data:
target_constraint = target.constraints.get(constraint)
if not target_constraint:
target_constraint = target.constraints.new(
data[constraint]['type'])
utils.dump_anything.load(
target_constraint, data[constraint])
def load_pose(target_bone, data):
target_bone.rotation_mode = data['rotation_mode']
utils.dump_anything.load(target_bone, data)
class BlObject(BlDatablock): class BlObject(BlDatablock):
bl_id = "objects"
bl_class = bpy.types.Object
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'OBJECT_DATA'
def construct(self, data): def construct(self, data):
pointer = None pointer = None
@ -42,24 +74,31 @@ class BlObject(BlDatablock):
elif data["data"] in bpy.data.speakers.keys(): elif data["data"] in bpy.data.speakers.keys():
pointer = bpy.data.speakers[data["data"]] pointer = bpy.data.speakers[data["data"]]
elif data["data"] in bpy.data.lightprobes.keys(): elif data["data"] in bpy.data.lightprobes.keys():
pass # Only supported since 2.83
# bpy need to support correct lightprobe creation from python if bpy.app.version[1] >= 83:
# pointer = bpy.data.lightprobes[data["data"]] pointer = bpy.data.lightprobes[data["data"]]
else:
logger.warning(
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
instance = bpy.data.objects.new(data["name"], pointer) instance = bpy.data.objects.new(data["name"], pointer)
instance.uuid = self.uuid instance.uuid = self.uuid
return instance return instance
def load(self, data, target): def load_implementation(self, data, target):
target.matrix_world = mathutils.Matrix(data["matrix_world"]) # Load transformation data
rot_mode = 'rotation_quaternion' if data['rotation_mode'] == 'QUATERNION' else 'rotation_euler'
target.rotation_mode = data['rotation_mode']
target.location = data['location']
setattr(target, rot_mode, data[rot_mode])
target.scale = data['scale']
target.name = data["name"] target.name = data["name"]
# Load modifiers # Load modifiers
if hasattr(target, 'modifiers'): if hasattr(target, 'modifiers'):
for local_modifier in target.modifiers: # TODO: smarter selective update
if local_modifier.name not in data['modifiers']: target.modifiers.clear()
target.modifiers.remove(local_modifier)
for modifier in data['modifiers']: for modifier in data['modifiers']:
target_modifier = target.modifiers.get(modifier) target_modifier = target.modifiers.get(modifier)
@ -70,6 +109,40 @@ class BlObject(BlDatablock):
utils.dump_anything.load( utils.dump_anything.load(
target_modifier, data['modifiers'][modifier]) target_modifier, data['modifiers'][modifier])
# Load constraints
# Object
if hasattr(target, 'constraints') and 'constraints' in data:
load_constraints(target, data['constraints'])
# Pose
if 'pose' in data:
if not target.pose:
raise Exception('No pose data yet (Fixed in a near futur)')
# Bone groups
for bg_name in data['pose']['bone_groups']:
bg_data = data['pose']['bone_groups'].get(bg_name)
bg_target = target.pose.bone_groups.get(bg_name)
if not bg_target:
bg_target = target.pose.bone_groups.new(name=bg_name)
utils.dump_anything.load(bg_target, bg_data)
# target.pose.bone_groups.get
# Bones
for bone in data['pose']['bones']:
target_bone = target.pose.bones.get(bone)
bone_data = data['pose']['bones'].get(bone)
if 'constraints' in bone_data.keys():
load_constraints(
target_bone, bone_data['constraints'])
load_pose(target_bone, bone_data)
if 'bone_index' in bone_data.keys():
target_bone.bone_group = target.pose.bone_group[bone_data['bone_group_index']]
# Load relations # Load relations
if 'children' in data.keys(): if 'children' in data.keys():
for child in data['children']: for child in data['children']:
@ -84,13 +157,43 @@ class BlObject(BlDatablock):
if data['instance_type'] == 'COLLECTION': if data['instance_type'] == 'COLLECTION':
target.instance_collection = bpy.data.collections[data['instance_collection']] target.instance_collection = bpy.data.collections[data['instance_collection']]
def dump(self, pointer=None): # vertex groups
if 'vertex_groups' in data:
target.vertex_groups.clear()
for vg in data['vertex_groups']:
vertex_group = target.vertex_groups.new(name=vg['name'])
for vert in vg['vertices']:
vertex_group.add(
[vert['index']], vert['weight'], 'REPLACE')
# SHAPE KEYS
if 'shape_keys' in data:
target.shape_key_clear()
object_data = target.data
# Create keys and load vertices coords
for key_block in data['shape_keys']['key_blocks']:
key_data = data['shape_keys']['key_blocks'][key_block]
target.shape_key_add(name=key_block)
utils.dump_anything.load(
target.data.shape_keys.key_blocks[key_block], key_data)
for vert in key_data['data']:
target.data.shape_keys.key_blocks[key_block].data[vert].co = key_data['data'][vert]['co']
# Load relative key after all
for key_block in data['shape_keys']['key_blocks']:
reference = data['shape_keys']['key_blocks'][key_block]['relative_key']
target.data.shape_keys.key_blocks[key_block].relative_key = target.data.shape_keys.key_blocks[reference]
def dump_implementation(self, data, pointer=None):
assert(pointer) assert(pointer)
dumper = utils.dump_anything.Dumper() dumper = utils.dump_anything.Dumper()
dumper.depth = 1 dumper.depth = 1
dumper.include_filter = [ dumper.include_filter = [
"name", "name",
"matrix_world",
"rotation_mode", "rotation_mode",
"parent", "parent",
"data", "data",
@ -99,8 +202,12 @@ class BlObject(BlDatablock):
"empty_display_type", "empty_display_type",
"empty_display_size", "empty_display_size",
"instance_collection", "instance_collection",
"instance_type" "instance_type",
"location",
"scale",
'rotation_quaternion' if pointer.rotation_mode == 'QUATERNION' else 'rotation_euler',
] ]
data = dumper.dump(pointer) data = dumper.dump(pointer)
if self.is_library: if self.is_library:
@ -109,8 +216,55 @@ class BlObject(BlDatablock):
# MODIFIERS # MODIFIERS
if hasattr(pointer, 'modifiers'): if hasattr(pointer, 'modifiers'):
dumper.include_filter = None dumper.include_filter = None
dumper.depth = 2
data["modifiers"] = {}
for index, modifier in enumerate(pointer.modifiers):
data["modifiers"][modifier.name] = dumper.dump(modifier)
data["modifiers"][modifier.name]['m_index'] = index
# CONSTRAINTS
# OBJECT
if hasattr(pointer, 'constraints'):
dumper.depth = 3 dumper.depth = 3
data["modifiers"] = dumper.dump(pointer.modifiers) data["constraints"] = dumper.dump(pointer.constraints)
# POSE
if hasattr(pointer, 'pose') and pointer.pose:
# BONES
bones = {}
for bone in pointer.pose.bones:
bones[bone.name] = {}
dumper.depth = 1
rotation = 'rotation_quaternion' if bone.rotation_mode == 'QUATERNION' else 'rotation_euler'
group_index = 'bone_group_index' if bone.bone_group else None
dumper.include_filter = [
'rotation_mode',
'location',
'scale',
'custom_shape',
'use_custom_shape_bone_size',
'custom_shape_scale',
group_index,
rotation
]
bones[bone.name] = dumper.dump(bone)
dumper.include_filter = []
dumper.depth = 3
bones[bone.name]["constraints"] = dumper.dump(bone.constraints)
data['pose'] = {'bones': bones}
# GROUPS
bone_groups = {}
for group in pointer.pose.bone_groups:
dumper.depth = 3
dumper.include_filter = [
'name',
'color_set'
]
bone_groups[group.name] = dumper.dump(group)
data['pose']['bone_groups'] = bone_groups
# CHILDS # CHILDS
if len(pointer.children) > 0: if len(pointer.children) > 0:
@ -120,13 +274,62 @@ class BlObject(BlDatablock):
data["children"] = childs data["children"] = childs
# VERTEx GROUP
if len(pointer.vertex_groups) > 0:
vg_data = []
for vg in pointer.vertex_groups:
vg_idx = vg.index
dumped_vg = {}
dumped_vg['name'] = vg.name
vertices = []
for v in pointer.data.vertices:
for vg in v.groups:
if vg.group == vg_idx:
vertices.append({
'index': v.index,
'weight': vg.weight
})
dumped_vg['vertices'] = vertices
vg_data.append(dumped_vg)
data['vertex_groups'] = vg_data
# SHAPE KEYS
pointer_data = pointer.data
if hasattr(pointer_data, 'shape_keys') and pointer_data.shape_keys:
dumper = utils.dump_anything.Dumper()
dumper.depth = 2
dumper.include_filter = [
'reference_key',
'use_relative'
]
data['shape_keys'] = dumper.dump(pointer_data.shape_keys)
data['shape_keys']['reference_key'] = pointer_data.shape_keys.reference_key.name
key_blocks = {}
for key in pointer_data.shape_keys.key_blocks:
dumper.depth = 3
dumper.include_filter = [
'name',
'data',
'mute',
'value',
'slider_min',
'slider_max',
'data',
'co'
]
key_blocks[key.name] = dumper.dump(key)
key_blocks[key.name]['relative_key'] = key.relative_key.name
data['shape_keys']['key_blocks'] = key_blocks
return data return data
def resolve(self):
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.objects)
def resolve_dependencies(self): def resolve_dependencies(self):
deps = [] deps = super().resolve_dependencies()
# Avoid Empty case # Avoid Empty case
if self.pointer.data: if self.pointer.data:
@ -138,19 +341,10 @@ class BlObject(BlDatablock):
deps.append(self.pointer.library) deps.append(self.pointer.library)
if self.pointer.instance_type == 'COLLECTION': if self.pointer.instance_type == 'COLLECTION':
#TODO: uuid based # TODO: uuid based
deps.append(self.pointer.instance_collection) deps.append(self.pointer.instance_collection)
return deps return deps
def is_valid(self): def is_valid(self):
return bpy.data.objects.get(self.data['name']) return bpy.data.objects.get(self.data['name'])
bl_id = "objects"
bl_class = bpy.types.Object
bl_rep_class = BlObject
bl_delay_refresh = 1
bl_delay_apply = 1
bl_automatic_push = True
bl_icon = 'OBJECT_DATA'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import sys
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR) logger.setLevel(logging.WARNING)
CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config") CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config")
CONFIG = os.path.join(CONFIG_DIR, "app.yaml") CONFIG = os.path.join(CONFIG_DIR, "app.yaml")
@ -48,12 +48,6 @@ def module_can_be_imported(name):
return False return False
def get_package_install_directory():
for path in sys.path:
if os.path.basename(path) in ("dist-packages", "site-packages"):
return path
def install_pip(): def install_pip():
# pip can not necessarily be imported into Blender after this # pip can not necessarily be imported into Blender after this
get_pip_path = Path(__file__).parent / "libs" / "get-pip.py" get_pip_path = Path(__file__).parent / "libs" / "get-pip.py"
@ -61,10 +55,8 @@ def install_pip():
def install_package(name): def install_package(name):
target = get_package_install_directory()
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", subprocess.run([str(PYTHON_PATH), "-m", "pip", "install",
name, '--target', target], cwd=SUBPROCESS_DIR) name], cwd=SUBPROCESS_DIR)
def check_dir(dir): def check_dir(dir):
if not os.path.exists(dir): if not os.path.exists(dir):

View File

@ -92,11 +92,13 @@ class Dumper:
def _build_inline_dump_functions(self): def _build_inline_dump_functions(self):
self._dump_identity = (lambda x, depth: x, lambda x, depth: x) self._dump_identity = (lambda x, depth: x, lambda x, depth: x)
self._dump_ref = (lambda x, depth: x.name, self._dump_object_as_branch)
self._dump_ID = (lambda x, depth: x.name, self._dump_default_as_branch) self._dump_ID = (lambda x, depth: x.name, self._dump_default_as_branch)
self._dump_collection = (self._dump_default_as_leaf, self._dump_collection_as_branch) self._dump_collection = (self._dump_default_as_leaf, self._dump_collection_as_branch)
self._dump_array = (self._dump_default_as_leaf, self._dump_array_as_branch) self._dump_array = (self._dump_default_as_leaf, self._dump_array_as_branch)
self._dump_matrix = (self._dump_matrix_as_leaf, self._dump_matrix_as_leaf) self._dump_matrix = (self._dump_matrix_as_leaf, self._dump_matrix_as_leaf)
self._dump_vector = (self._dump_vector_as_leaf, self._dump_vector_as_leaf) self._dump_vector = (self._dump_vector_as_leaf, self._dump_vector_as_leaf)
self._dump_quaternion = (self._dump_quaternion_as_leaf, self._dump_quaternion_as_leaf)
self._dump_default = (self._dump_default_as_leaf, self._dump_default_as_branch) self._dump_default = (self._dump_default_as_leaf, self._dump_default_as_branch)
self._dump_color = (self._dump_color_as_leaf, self._dump_color_as_leaf) self._dump_color = (self._dump_color_as_leaf, self._dump_color_as_leaf)
@ -105,11 +107,14 @@ class Dumper:
self._match_type_int = (_dump_filter_type(int), self._dump_identity) self._match_type_int = (_dump_filter_type(int), self._dump_identity)
self._match_type_float = (_dump_filter_type(float), self._dump_identity) self._match_type_float = (_dump_filter_type(float), self._dump_identity)
self._match_type_string = (_dump_filter_type(str), self._dump_identity) self._match_type_string = (_dump_filter_type(str), self._dump_identity)
self._match_type_ref = (_dump_filter_type(T.Object), self._dump_ref)
self._match_type_ID = (_dump_filter_type(T.ID), self._dump_ID) self._match_type_ID = (_dump_filter_type(T.ID), self._dump_ID)
self._match_type_bpy_prop_collection = (_dump_filter_type(T.bpy_prop_collection), self._dump_collection) self._match_type_bpy_prop_collection = (_dump_filter_type(T.bpy_prop_collection), self._dump_collection)
self._match_type_array = (_dump_filter_array, self._dump_array) self._match_type_array = (_dump_filter_array, self._dump_array)
self._match_type_matrix = (_dump_filter_type(mathutils.Matrix), self._dump_matrix) self._match_type_matrix = (_dump_filter_type(mathutils.Matrix), self._dump_matrix)
self._match_type_vector = (_dump_filter_type(mathutils.Vector), self._dump_vector) self._match_type_vector = (_dump_filter_type(mathutils.Vector), self._dump_vector)
self._match_type_quaternion = (_dump_filter_type(mathutils.Quaternion), self._dump_quaternion)
self._match_type_euler = (_dump_filter_type(mathutils.Euler), self._dump_quaternion)
self._match_type_color = (_dump_filter_type_by_name("Color"), self._dump_color) self._match_type_color = (_dump_filter_type_by_name("Color"), self._dump_color)
self._match_default = (_dump_filter_default, self._dump_default) self._match_default = (_dump_filter_default, self._dump_default)
@ -136,9 +141,18 @@ class Dumper:
def _dump_vector_as_leaf(self, vector, depth): def _dump_vector_as_leaf(self, vector, depth):
return list(vector) return list(vector)
def _dump_quaternion_as_leaf(self, quaternion, depth):
return list(quaternion)
def _dump_color_as_leaf(self, color, depth): def _dump_color_as_leaf(self, color, depth):
return list(color) return list(color)
def _dump_object_as_branch(self, default, depth):
if depth == 1:
return self._dump_default_as_branch(default, depth)
else:
return default.name
def _dump_default_as_branch(self, default, depth): def _dump_default_as_branch(self, default, depth):
def is_valid_property(p): def is_valid_property(p):
try: try:
@ -173,12 +187,14 @@ class Dumper:
self._match_type_int, self._match_type_int,
self._match_type_float, self._match_type_float,
self._match_type_string, self._match_type_string,
self._match_type_ref,
self._match_type_ID, self._match_type_ID,
self._match_type_bpy_prop_collection, self._match_type_bpy_prop_collection,
self._match_type_array, self._match_type_array,
self._match_type_matrix, self._match_type_matrix,
self._match_type_vector, self._match_type_vector,
self._match_type_color, self._match_type_quaternion,
self._match_type_euler,
self._match_type_color, self._match_type_color,
self._match_default self._match_default
] ]
@ -307,6 +323,12 @@ class Loader:
def _load_vector(self, vector, dump): def _load_vector(self, vector, dump):
vector.write(mathutils.Vector(dump)) vector.write(mathutils.Vector(dump))
def _load_quaternion(self, quaternion, dump):
quaternion.write(mathutils.Quaternion(dump))
def _load_euler(self, euler, dump):
euler.write(mathutils.Euler(dump))
def _ordered_keys(self, keys): def _ordered_keys(self, keys):
ordered_keys = [] ordered_keys = []
for order_element in self.order: for order_element in self.order:
@ -336,6 +358,8 @@ class Loader:
(_load_filter_type(T.IntProperty), self._load_identity), (_load_filter_type(T.IntProperty), self._load_identity),
(_load_filter_type(mathutils.Matrix, use_bl_rna=False), self._load_matrix), # before float because bl_rna type of matrix if FloatProperty (_load_filter_type(mathutils.Matrix, use_bl_rna=False), self._load_matrix), # before float because bl_rna type of matrix if FloatProperty
(_load_filter_type(mathutils.Vector, use_bl_rna=False), self._load_vector), # before float because bl_rna type of vector if FloatProperty (_load_filter_type(mathutils.Vector, use_bl_rna=False), self._load_vector), # before float because bl_rna type of vector if FloatProperty
(_load_filter_type(mathutils.Quaternion, use_bl_rna=False), self._load_quaternion),
(_load_filter_type(mathutils.Euler, use_bl_rna=False), self._load_euler),
(_load_filter_type(T.FloatProperty), self._load_identity), (_load_filter_type(T.FloatProperty), self._load_identity),
(_load_filter_type(T.StringProperty), self._load_identity), (_load_filter_type(T.StringProperty), self._load_identity),
(_load_filter_type(T.EnumProperty), self._load_identity), (_load_filter_type(T.EnumProperty), self._load_identity),

View File

@ -4,53 +4,51 @@ import os
import queue import queue
import random import random
import string import string
import subprocess
import time import time
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
from subprocess import PIPE, Popen, TimeoutExpired
import msgpack
import bpy import bpy
import mathutils import mathutils
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from bpy_extras.io_utils import ExportHelper
from . import bl_types, delayable, environment, presence, ui, utils from . import bl_types, delayable, environment, presence, ui, utils
from .libs.replication.replication.constants import (FETCHED, STATE_ACTIVE,
STATE_INITIAL,
STATE_SYNCING)
from .libs.replication.replication.data import ReplicatedDataFactory from .libs.replication.replication.data import ReplicatedDataFactory
from .libs.replication.replication.exception import NonAuthorizedOperationError from .libs.replication.replication.exception import NonAuthorizedOperationError
from .libs.replication.replication.interface import Session from .libs.replication.replication.interface import Session
from .libs.replication.replication.constants import (
STATE_ACTIVE,
STATE_INITIAL,
STATE_SYNCING)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR) logger.setLevel(logging.WARNING)
client = None client = None
delayables = [] delayables = []
ui_context = None ui_context = None
stop_modal_executor = False
modal_executor_queue = None
server_process = None
def unregister_delayables():
global delayables, stop_modal_executor
def init_supported_datablocks(supported_types_id): for d in delayables:
global client try:
d.unregister()
for type_id in supported_types_id: except:
if hasattr(bpy.data, type_id):
for item in getattr(bpy.data, type_id):
if client.exist(uuid=item.uuid):
continue continue
else:
client.add(item)
stop_modal_executor = True
# OPERATORS # OPERATORS
class SessionStartOperator(bpy.types.Operator): class SessionStartOperator(bpy.types.Operator):
bl_idname = "session.start" bl_idname = "session.start"
bl_label = "start" bl_label = "start"
bl_description = "connect to a net server" bl_description = "connect to a net server"
bl_options = {"REGISTER"}
host: bpy.props.BoolProperty(default=False) host: bpy.props.BoolProperty(default=False)
@ -59,94 +57,92 @@ class SessionStartOperator(bpy.types.Operator):
return True return True
def execute(self, context): def execute(self, context):
global client, delayables global client, delayables, ui_context, server_process
settings = context.window_manager.session settings = context.window_manager.session
users = bpy.data.window_managers['WinMan'].online_users
# TODO: Sync server clients
users.clear()
delayables.clear()
# save config # save config
settings.save(context) settings.save(context)
bpy_factory = ReplicatedDataFactory() bpy_factory = ReplicatedDataFactory()
supported_bl_types = [] supported_bl_types = []
ui_context = context.copy()
# init the factory with supported types # init the factory with supported types
for type in bl_types.types_to_register(): for type in bl_types.types_to_register():
_type = getattr(bl_types, type) type_module = getattr(bl_types, type)
supported_bl_types.append(_type.bl_id) type_impl_name = "Bl{}".format(type.split('_')[1].capitalize())
type_module_class = getattr(type_module, type_impl_name)
supported_bl_types.append(type_module_class.bl_id)
# Retreive local replicated types settings # Retreive local replicated types settings
type_local_config = settings.supported_datablock[_type.bl_rep_class.__name__] type_local_config = settings.supported_datablock[type_impl_name]
bpy_factory.register_type( bpy_factory.register_type(
_type.bl_class, type_module_class.bl_class,
_type.bl_rep_class, type_module_class,
timer=type_local_config.bl_delay_refresh, timer=type_local_config.bl_delay_refresh,
automatic=type_local_config.auto_push) automatic=type_local_config.auto_push)
if type_local_config.bl_delay_apply > 0: if type_local_config.bl_delay_apply > 0:
delayables.append(delayable.ApplyTimer( delayables.append(delayable.ApplyTimer(
timout=type_local_config.bl_delay_apply, timout=type_local_config.bl_delay_apply,
target_type=_type.bl_rep_class)) target_type=type_module_class))
client = Session(factory=bpy_factory) client = Session(
factory=bpy_factory,
python_path=bpy.app.binary_path_python,
default_strategy=settings.right_strategy)
# Host a session
if self.host: if self.host:
# Scene setup # Scene setup
if settings.start_empty: if settings.start_empty:
utils.clean_scene() utils.clean_scene()
try:
for scene in bpy.data.scenes:
scene_uuid = client.add(scene)
client.commit(scene_uuid)
client.host( client.host(
id=settings.username, id=settings.username,
address=settings.ip, address=settings.ip,
port=settings.port, port=settings.port,
right_strategy=settings.right_strategy ipc_port=settings.ipc_port)
) except Exception as e:
self.report({'ERROR'}, repr(e))
logger.error(f"Error: {e}")
finally:
settings.is_admin = True settings.is_admin = True
# Join a session
else: else:
utils.clean_scene() utils.clean_scene()
try:
client.connect( client.connect(
id=settings.username, id=settings.username,
address=settings.ip, address=settings.ip,
port=settings.port port=settings.port,
ipc_port=settings.ipc_port
) )
except Exception as e:
time.sleep(1) self.report({'ERROR'}, repr(e))
logger.error(f"Error: {e}")
if client.state == 0: finally:
settings.is_admin = False settings.is_admin = False
self.report(
{'ERROR'},
"A session is already hosted on this address")
return {"CANCELLED"}
# Background client updates service
#TODO: Refactoring
# Init user settings delayables.append(delayable.ClientUpdate())
usr = presence.User(
username=settings.username,
color=(settings.client_color.r,
settings.client_color.g,
settings.client_color.b,
1),
)
settings.user_uuid = client.add(usr,owner=settings.username)
client.commit(settings.user_uuid)
if settings.init_scene and self.host:
for scene in bpy.data.scenes:
scene_uuid = client.add(scene)
# for node in client.list():
client.commit(scene_uuid)
delayables.append(delayable.ClientUpdate(
client_uuid=settings.user_uuid))
delayables.append(delayable.DrawClient()) delayables.append(delayable.DrawClient())
delayables.append(delayable.DynamicRightSelectTimer()) delayables.append(delayable.DynamicRightSelectTimer())
# Push all added values
client.push_all()
# Launch drawing module # Launch drawing module
if settings.enable_presence: if settings.enable_presence:
presence.renderer.run() presence.renderer.run()
@ -155,6 +151,10 @@ class SessionStartOperator(bpy.types.Operator):
for d in delayables: for d in delayables:
d.register() d.register()
global modal_executor_queue
modal_executor_queue = queue.Queue()
bpy.ops.session.apply_armature_operator()
self.report( self.report(
{'INFO'}, {'INFO'},
"connexion on tcp://{}:{}".format(settings.ip, settings.port)) "connexion on tcp://{}:{}".format(settings.ip, settings.port))
@ -172,20 +172,13 @@ class SessionStopOperator(bpy.types.Operator):
return True return True
def execute(self, context): def execute(self, context):
global client, delayables global client, delayables, stop_modal_executor
settings = context.window_manager.session
settings.is_admin = False
assert(client) assert(client)
client.remove(settings.user_uuid)
client.disconnect()
for d in delayables:
try: try:
d.unregister() client.disconnect()
except: except Exception as e:
continue self.report({'ERROR'}, repr(e))
presence.renderer.stop()
return {"FINISHED"} return {"FINISHED"}
@ -264,6 +257,14 @@ class SessionSnapUserOperator(bpy.types.Operator):
def execute(self, context): def execute(self, context):
wm = context.window_manager wm = context.window_manager
settings = context.window_manager.session
if settings.time_snap_running:
settings.time_snap_running = False
return {'CANCELLED'}
else:
settings.time_snap_running = True
self._timer = wm.event_timer_add(0.1, window=context.window) self._timer = wm.event_timer_add(0.1, window=context.window)
wm.modal_handler_add(self) wm.modal_handler_add(self)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
@ -273,7 +274,9 @@ class SessionSnapUserOperator(bpy.types.Operator):
wm.event_timer_remove(self._timer) wm.event_timer_remove(self._timer)
def modal(self, context, event): def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}: is_running = context.window_manager.session.time_snap_running
if event.type in {'RIGHTMOUSE', 'ESC'} or not is_running:
self.cancel(context) self.cancel(context)
return {'CANCELLED'} return {'CANCELLED'}
@ -281,9 +284,64 @@ class SessionSnapUserOperator(bpy.types.Operator):
area, region, rv3d = presence.view3d_find() area, region, rv3d = presence.view3d_find()
global client global client
target_client = client.get(uuid=self.target_client) if client:
if target_client: target_ref = client.online_users.get(self.target_client)
rv3d.view_matrix = mathutils.Matrix(target_client.data['view_matrix'])
if target_ref:
rv3d.view_matrix = mathutils.Matrix(
target_ref['metadata']['view_matrix'])
else:
return {"CANCELLED"}
return {'PASS_THROUGH'}
class SessionSnapTimeOperator(bpy.types.Operator):
bl_idname = "session.snaptime"
bl_label = "snap to user time"
bl_description = "Snap time to selected user time's"
bl_options = {"REGISTER"}
_timer = None
target_client: bpy.props.StringProperty(default="None")
@classmethod
def poll(cls, context):
return True
def execute(self, context):
settings = context.window_manager.session
if settings.user_snap_running:
settings.user_snap_running = False
return {'CANCELLED'}
else:
settings.user_snap_running = True
wm = context.window_manager
self._timer = wm.event_timer_add(0.05, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
def modal(self, context, event):
is_running = context.window_manager.session.user_snap_running
if event.type in {'RIGHTMOUSE', 'ESC'} or not is_running:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
global client
if client:
target_ref = client.online_users.get(self.target_client)
if target_ref:
context.scene.frame_current = target_ref['metadata']['frame_current']
else: else:
return {"CANCELLED"} return {"CANCELLED"}
@ -330,23 +388,130 @@ class SessionCommit(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class ApplyArmatureOperator(bpy.types.Operator):
"""Operator which runs its self from a timer"""
bl_idname = "session.apply_armature_operator"
bl_label = "Modal Executor Operator"
_timer = None
def modal(self, context, event):
global stop_modal_executor, modal_executor_queue
if stop_modal_executor:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'TIMER':
global client
if client and client.state['STATE'] == STATE_ACTIVE:
nodes = client.list(filter=bl_types.bl_armature.BlArmature)
for node in nodes:
node_ref = client.get(uuid=node)
if node_ref.state == FETCHED:
try:
client.apply(node)
except Exception as e:
logger.error(
"fail to apply {}: {}".format(node_ref.uuid, e))
return {'PASS_THROUGH'}
def execute(self, context):
wm = context.window_manager
self._timer = wm.event_timer_add(2, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
global stop_modal_executor
wm = context.window_manager
wm.event_timer_remove(self._timer)
stop_modal_executor = False
classes = ( classes = (
SessionStartOperator, SessionStartOperator,
SessionStopOperator, SessionStopOperator,
SessionPropertyRemoveOperator, SessionPropertyRemoveOperator,
SessionSnapUserOperator, SessionSnapUserOperator,
SessionSnapTimeOperator,
SessionPropertyRightOperator, SessionPropertyRightOperator,
SessionApply, SessionApply,
SessionCommit, SessionCommit,
ApplyArmatureOperator,
) )
@persistent @persistent
def load_pre_handler(dummy): def load_pre_handler(dummy):
global client global client
if client and client.state in [STATE_ACTIVE, STATE_SYNCING]: if client and client.state['STATE'] in [STATE_ACTIVE, STATE_SYNCING]:
bpy.ops.session.stop() bpy.ops.session.stop()
@persistent
def sanitize_deps_graph(dummy):
"""sanitize deps graph
Temporary solution to resolve each node pointers after a Undo.
A future solution should be to avoid storing dataclock reference...
"""
global client
if client and client.state['STATE'] in [STATE_ACTIVE]:
for node_key in client.list():
client.get(node_key).resolve()
@persistent
def update_client_frame(scene):
if client and client.state['STATE'] == STATE_ACTIVE:
client.update_user_metadata({
'frame_current': scene.frame_current
})
@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]
session_infos = bpy.context.window_manager.session
# 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.owner == session_infos.username:
# Avoid slow geometry update
if 'EDIT' in context.mode:
break
logger.error("UPDATE: MODIFIFY {}".format(type(update.id)))
# client.commit(node.uuid)
# client.push(node.uuid)
else:
# Distant update
continue
# else:
# # New items !
# logger.error("UPDATE: ADD")C.obj
def register(): def register():
from bpy.utils import register_class from bpy.utils import register_class
for cls in classes: for cls in classes:
@ -354,10 +519,18 @@ def register():
bpy.app.handlers.load_pre.append(load_pre_handler) bpy.app.handlers.load_pre.append(load_pre_handler)
bpy.app.handlers.undo_post.append(sanitize_deps_graph)
bpy.app.handlers.redo_post.append(sanitize_deps_graph)
bpy.app.handlers.frame_change_pre.append(update_client_frame)
# bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
def unregister(): def unregister():
global client global client
if client and client.state == 2: if client and client.state['STATE'] == 2:
client.disconnect() client.disconnect()
client = None client = None
@ -367,5 +540,13 @@ def unregister():
bpy.app.handlers.load_pre.remove(load_pre_handler) bpy.app.handlers.load_pre.remove(load_pre_handler)
bpy.app.handlers.undo_post.remove(sanitize_deps_graph)
bpy.app.handlers.redo_post.remove(sanitize_deps_graph)
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
# bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation)
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View File

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

View File

@ -1,9 +1,12 @@
import bpy import bpy
from . import operators from . import operators
from .bl_types.bl_user import BlUser
from .libs.replication.replication.constants import (ADDED, ERROR, FETCHED, from .libs.replication.replication.constants import (ADDED, ERROR, FETCHED,
MODIFIED, RP_COMMON, UP) MODIFIED, RP_COMMON, UP,
STATE_ACTIVE, STATE_AUTH,
STATE_CONFIG, STATE_SYNCING,
STATE_INITIAL, STATE_SRV_SYNC,
STATE_WAITING, STATE_QUITTING)
ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
'TRIA_UP', # COMMITED 'TRIA_UP', # COMMITED
@ -12,6 +15,44 @@ ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
'FILE_REFRESH', # UP 'FILE_REFRESH', # UP
'TRIA_UP'] # CHANGED 'TRIA_UP'] # CHANGED
def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '', fill_empty=' '):
"""
Call in a loop to create terminal progress bar
@params:
iteration - Required : current iteration (Int)
total - Required : total iterations (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
length - Optional : character length of bar (Int)
fill - Optional : bar fill character (Str)
From here:
https://gist.github.com/greenstick/b23e475d2bfdc3a82e34eaa1f6781ee4
"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filledLength = int(length * iteration // total)
bar = fill * filledLength + fill_empty * (length - filledLength)
return '{} |{}| {}/{}{}'.format(prefix, bar, iteration,total, suffix)
def get_state_str(state):
state_str = 'None'
if state == STATE_WAITING:
state_str = 'WARMING UP DATA'
elif state == STATE_SYNCING:
state_str = 'FETCHING FROM SERVER'
elif state == STATE_AUTH:
state_str = 'AUTHENTIFICATION'
elif state == STATE_CONFIG:
state_str = 'CONFIGURATION'
elif state == STATE_ACTIVE:
state_str = 'ONLINE'
elif state == STATE_SRV_SYNC:
state_str = 'PUSHING TO SERVER'
elif state == STATE_INITIAL:
state_str = 'INIT'
elif state == STATE_QUITTING:
state_str = 'QUITTING SESSION'
return state_str
class SESSION_PT_settings(bpy.types.Panel): class SESSION_PT_settings(bpy.types.Panel):
"""Settings panel""" """Settings panel"""
@ -32,22 +73,55 @@ class SESSION_PT_settings(bpy.types.Panel):
if hasattr(context.window_manager, 'session'): if hasattr(context.window_manager, 'session'):
# STATE INITIAL # STATE INITIAL
if not operators.client \ if not operators.client \
or (operators.client and operators.client.state == 0): or (operators.client and operators.client.state['STATE'] == STATE_INITIAL):
pass pass
else: else:
# STATE ACTIVE cli_state = operators.client.state
if operators.client.state == 2:
row.label(text=f"Status : {get_state_str(cli_state['STATE'])}")
row = layout.row() row = layout.row()
current_state = cli_state['STATE']
# STATE ACTIVE
if current_state == STATE_ACTIVE:
row.operator("session.stop", icon='QUIT', text="Exit") row.operator("session.stop", icon='QUIT', text="Exit")
row = layout.row() row = layout.row()
# STATE SYNCING # CONNECTION STATE
else: elif current_state in [
status = "connecting..." STATE_SRV_SYNC,
row.label(text=status) STATE_SYNCING,
STATE_AUTH,
STATE_CONFIG,
STATE_WAITING]:
if cli_state['STATE'] in [STATE_SYNCING,STATE_SRV_SYNC,STATE_WAITING]:
box = row.box()
box.label(text=printProgressBar(
cli_state['CURRENT'],
cli_state['TOTAL'],
length=16
))
row = layout.row() row = layout.row()
row.operator("session.stop", icon='QUIT', text="CANCEL") row.operator("session.stop", icon='QUIT', text="CANCEL")
elif current_state == STATE_QUITTING:
row = layout.row()
box = row.box()
num_online_services = 0
for name, state in operators.client.services_state.items():
if state == STATE_ACTIVE:
num_online_services += 1
total_online_services = len(operators.client.services_state)
box.label(text=printProgressBar(
total_online_services-num_online_services,
total_online_services,
length=16
))
class SESSION_PT_settings_network(bpy.types.Panel): class SESSION_PT_settings_network(bpy.types.Panel):
bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel" bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel"
@ -60,7 +134,7 @@ class SESSION_PT_settings_network(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return not operators.client \ return not operators.client \
or (operators.client and operators.client.state == 0) or (operators.client and operators.client.state['STATE'] == 0)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@ -72,24 +146,24 @@ class SESSION_PT_settings_network(bpy.types.Panel):
row.prop(settings, "session_mode", expand=True) row.prop(settings, "session_mode", expand=True)
row = layout.row() row = layout.row()
if settings.session_mode == 'HOST':
box = row.box()
row = box.row()
row.label(text="Start empty:")
row.prop(settings, "start_empty", text="")
row = box.row()
row.label(text="Port:")
row.prop(settings, "port", text="")
row = box.row()
row.operator("session.start", text="HOST").host = True
else:
box = row.box() box = row.box()
row = box.row() row = box.row()
row.prop(settings, "ip", text="IP") row.prop(settings, "ip", text="IP")
row = box.row() row = box.row()
row.label(text="Port:") row.label(text="Port:")
row.prop(settings, "port", text="") row.prop(settings, "port", text="")
row = box.row()
row.label(text="IPC Port:")
row.prop(settings, "ipc_port", text="")
if settings.session_mode == 'HOST':
row = box.row()
row.label(text="Start empty:")
row.prop(settings, "start_empty", text="")
row = box.row()
row.operator("session.start", text="HOST").host = True
else:
row = box.row() row = box.row()
row.operator("session.start", text="CONNECT").host = False row.operator("session.start", text="CONNECT").host = False
@ -105,7 +179,7 @@ class SESSION_PT_settings_user(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return not operators.client \ return not operators.client \
or (operators.client and operators.client.state == 0) or (operators.client and operators.client.state['STATE'] == 0)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@ -133,7 +207,7 @@ class SESSION_PT_settings_replication(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return not operators.client \ return not operators.client \
or (operators.client and operators.client.state == 0) or (operators.client and operators.client.state['STATE'] == 0)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@ -167,7 +241,7 @@ class SESSION_PT_settings_replication(bpy.types.Panel):
class SESSION_PT_user(bpy.types.Panel): class SESSION_PT_user(bpy.types.Panel):
bl_idname = "MULTIUSER_USER_PT_panel" bl_idname = "MULTIUSER_USER_PT_panel"
bl_label = "Users" bl_label = "Online users"
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = "Multiuser" bl_category = "Multiuser"
@ -175,48 +249,62 @@ class SESSION_PT_user(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return operators.client and operators.client.state == 2 return operators.client and operators.client.state['STATE'] == 2
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
online_users = context.window_manager.online_users
selected_user = context.window_manager.user_index
settings = context.window_manager.session settings = context.window_manager.session
active_user = online_users[selected_user] if len(online_users)-1>=selected_user else 0
# Create a simple row. # Create a simple row.
col = layout.column(align=True) row = layout.row()
box = row.box()
split = box.split(factor=0.5)
split.label(text="user")
split.label(text="frame")
split.label(text="ping")
client_keys = operators.client.list(filter=BlUser) row = layout.row()
if client_keys and len(client_keys) > 0: layout.template_list("SESSION_UL_users", "", context.window_manager, "online_users", context.window_manager, "user_index")
for key in client_keys:
area_msg = col.row(align=True)
item_box = area_msg.box()
client = operators.client.get(uuid=key).data
info = "" if active_user != 0 and active_user.username != settings.username:
row = layout.row()
detail_item_row = item_box.row(align=True) user_operations = row.split()
user_operations.alert = context.window_manager.session.time_snap_running
if client.get('name'): user_operations.operator(
username = client['name']
is_local_user = username == settings.username
if is_local_user:
info = "(self)"
detail_item_row.label(
text="{} {}".format(username, info))
if not is_local_user:
detail_item_row.operator(
"session.snapview", "session.snapview",
text="", text="",
icon='VIEW_CAMERA').target_client = key icon='VIEW_CAMERA').target_client = active_user.username
row = layout.row()
else:
row.label(text="Empty")
row = layout.row() user_operations.alert = context.window_manager.session.user_snap_running
user_operations.operator(
"session.snaptime",
text="",
icon='TIME').target_client = active_user.username
class SESSION_UL_users(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
session = operators.client
settings = context.window_manager.session
is_local_user = item.username == settings.username
ping = '-'
frame_current = '-'
if session:
user = session.online_users.get(item.username)
if user:
ping = str(user['latency'])
metadata = user.get('metadata')
if metadata and 'frame_current' in metadata:
frame_current = str(metadata['frame_current'])
split = layout.split(factor=0.5)
split.label(text=item.username)
split.label(text=frame_current)
split.label(text=ping)
class SESSION_PT_presence(bpy.types.Panel): class SESSION_PT_presence(bpy.types.Panel):
@ -230,7 +318,8 @@ class SESSION_PT_presence(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return True return not operators.client \
or (operators.client and operators.client.state['STATE'] in [STATE_INITIAL, STATE_ACTIVE])
def draw_header(self, context): def draw_header(self, context):
self.layout.prop(context.window_manager.session, "enable_presence", text="") self.layout.prop(context.window_manager.session, "enable_presence", text="")
@ -245,12 +334,40 @@ class SESSION_PT_presence(bpy.types.Panel):
col.prop(settings,"presence_show_user") col.prop(settings,"presence_show_user")
row = layout.row() row = layout.row()
class SESSION_PT_services(bpy.types.Panel):
bl_idname = "MULTIUSER_SERVICE_PT_panel"
bl_label = "Services"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Multiuser"
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return operators.client and operators.client.state['STATE'] == 2
def draw(self, context):
layout = self.layout
online_users = context.window_manager.online_users
selected_user = context.window_manager.user_index
settings = context.window_manager.session
active_user = online_users[selected_user] if len(online_users)-1>=selected_user else 0
# Create a simple row.
for name, state in operators.client.services_state.items():
row = layout.row()
row.label(text=name)
row.label(text=get_state_str(state))
def draw_property(context, parent, property_uuid, level=0): def draw_property(context, parent, property_uuid, level=0):
settings = context.window_manager.session settings = context.window_manager.session
item = operators.client.get(uuid=property_uuid) item = operators.client.get(uuid=property_uuid)
if item.str_type == 'BlUser' or item.state == ERROR: if item.state == ERROR:
return return
area_msg = parent.row(align=True) area_msg = parent.row(align=True)
@ -317,7 +434,7 @@ class SESSION_PT_outliner(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return operators.client and operators.client.state == 2 return operators.client and operators.client.state['STATE'] == 2
def draw_header(self, context): def draw_header(self, context):
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE') self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
@ -365,13 +482,15 @@ class SESSION_PT_outliner(bpy.types.Panel):
classes = ( classes = (
SESSION_UL_users,
SESSION_PT_settings, SESSION_PT_settings,
SESSION_PT_settings_user, SESSION_PT_settings_user,
SESSION_PT_settings_network, SESSION_PT_settings_network,
SESSION_PT_presence, SESSION_PT_presence,
SESSION_PT_settings_replication, SESSION_PT_settings_replication,
SESSION_PT_user, SESSION_PT_user,
SESSION_PT_outliner SESSION_PT_outliner,
SESSION_PT_services
) )

View File

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