Merge branch 'develop' into 'master'
v0.0.2 Closes #69 See merge request slumber/multi-user!19
3
.gitignore
vendored
@ -7,3 +7,6 @@ __pycache__/
|
||||
cache
|
||||
config
|
||||
*.code-workspace
|
||||
|
||||
# sphinx build folder
|
||||
_build
|
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
||||
[submodule "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
@ -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.
|
23
README.md
@ -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.
|
||||
|
||||
## Installation
|
||||
## Quick installation
|
||||
|
||||
1. Download latest release [multi_user.zip](/uploads/8aef79c7cf5b1d9606dc58307fd9ad8b/multi_user.zip).
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
|
||||
|
||||
| Name | Status | Comment |
|
||||
| ---------- | :----------------: | :------------: |
|
||||
| action | :x: | WIP |
|
||||
| armature | :x: | WIP |
|
||||
| ----------- | :----------------: | :------------: |
|
||||
| action | :exclamation: | Not stable |
|
||||
| armature | :exclamation: | Not stable |
|
||||
| camera | :white_check_mark: | |
|
||||
| collection | :white_check_mark: | |
|
||||
| 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 |
|
||||
| mesh | :white_check_mark: | |
|
||||
| material | :white_check_mark: | |
|
||||
| metaball | :x: | |
|
||||
| metaball | :white_check_mark: | |
|
||||
| object | :white_check_mark: | |
|
||||
| scene | :white_check_mark: | |
|
||||
| world | :white_check_mark: | |
|
||||
| lightprobes | :white_check_mark: | |
|
||||
|
||||
### 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.
|
||||
|
||||
## Dependencies
|
||||
@ -58,12 +59,10 @@ I'm working on it.
|
||||
|
||||
## Contributing
|
||||
|
||||
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
|
||||
See [contributing section](https://multi-user.readthedocs.io/en/latest/ways_to_contribute.html) of the documentation.
|
||||
|
||||
## Licensing
|
||||
|
||||
See [license](LICENSE)
|
||||
|
||||
[](https://multi-user.readthedocs.io/en/latest/?badge=latest)
|
||||
|
20
docs/Makefile
Normal 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
@ -0,0 +1,8 @@
|
||||
About
|
||||
=====
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:name: toc-about
|
||||
|
||||
introduction
|
10
docs/about/introduction.rst
Normal 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
@ -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
|
BIN
docs/getting_started/img/quickstart_advanced.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
docs/getting_started/img/quickstart_host.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
docs/getting_started/img/quickstart_join.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
docs/getting_started/img/quickstart_properties.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/getting_started/img/quickstart_pull.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
docs/getting_started/img/quickstart_push.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
docs/getting_started/img/quickstart_refresh.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
docs/getting_started/img/quickstart_remove.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
docs/getting_started/img/quickstart_unlock.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
docs/getting_started/img/quickstart_user_infos.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
docs/getting_started/img/quickstart_users.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
9
docs/getting_started/index.rst
Normal file
@ -0,0 +1,9 @@
|
||||
Getting started
|
||||
===============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:name: toc-getting-started
|
||||
|
||||
install
|
||||
quickstart
|
9
docs/getting_started/install.rst
Normal 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.
|
111
docs/getting_started/quickstart.rst
Normal 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
After Width: | Height: | Size: 409 KiB |
BIN
docs/img/homepage_roadmap.png
Normal file
After Width: | Height: | Size: 274 KiB |
59
docs/index.rst
Normal 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
@ -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
|
47
docs/tutorials/hosting_guide.rst
Normal 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.
|
||||
|
||||
|
||||
|
||||
|
BIN
docs/tutorials/img/hosting_guide_port.png
Normal file
After Width: | Height: | Size: 14 KiB |
8
docs/tutorials/index.rst
Normal file
@ -0,0 +1,8 @@
|
||||
Tutorials
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:name: toc-tutorial
|
||||
|
||||
hosting_guide
|
42
docs/ways_to_contribute.rst
Normal 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
|
@ -1,11 +1,15 @@
|
||||
bl_info = {
|
||||
"name": "Multi-User",
|
||||
"author": "Swann Martinez",
|
||||
"description": "",
|
||||
"version": (0, 0, 2),
|
||||
"description": "Enable real-time collaborative workflow inside blender",
|
||||
"blender": (2, 80, 0),
|
||||
"location": "",
|
||||
"location": "3D View > Sidebar > Multi-User tab",
|
||||
"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.setLevel(logging.ERROR)
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
#TODO: refactor config
|
||||
# UTILITY FUNCTIONS
|
||||
def generate_supported_types():
|
||||
stype_dict = {'supported_types':{}}
|
||||
for type in bl_types.types_to_register():
|
||||
_type = getattr(bl_types, type)
|
||||
props = {}
|
||||
props['bl_delay_refresh']=_type.bl_delay_refresh
|
||||
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
|
||||
type_module = getattr(bl_types, type)
|
||||
type_impl_name = "Bl{}".format(type.split('_')[1].capitalize())
|
||||
type_module_class = getattr(type_module, type_impl_name)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def client_list_callback(scene, context):
|
||||
from . import operators
|
||||
from .bl_types.bl_user import BlUser
|
||||
|
||||
items = [(RP_COMMON, RP_COMMON, "")]
|
||||
|
||||
username = bpy.context.window_manager.session.username
|
||||
cli = operators.client
|
||||
if cli:
|
||||
client_keys = cli.list(filter=BlUser)
|
||||
for k in client_keys:
|
||||
name = cli.get(uuid=k).data["name"]
|
||||
|
||||
name_desc = name
|
||||
if name == username:
|
||||
client_ids = cli.online_users.keys()
|
||||
for id in client_ids:
|
||||
name_desc = id
|
||||
if id == username:
|
||||
name_desc += " (self)"
|
||||
|
||||
items.append((name, name_desc, ""))
|
||||
items.append((id, name_desc, ""))
|
||||
|
||||
return items
|
||||
|
||||
@ -90,6 +94,15 @@ class ReplicatedDatablock(bpy.types.PropertyGroup):
|
||||
auto_push: bpy.props.BoolProperty(default=True)
|
||||
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):
|
||||
username: bpy.props.StringProperty(
|
||||
name="Username",
|
||||
@ -109,25 +122,19 @@ class SessionProps(bpy.types.PropertyGroup):
|
||||
description='Distant host port',
|
||||
default=5555
|
||||
)
|
||||
add_property_depth: bpy.props.IntProperty(
|
||||
name="add_property_depth",
|
||||
default=1
|
||||
ipc_port: bpy.props.IntProperty(
|
||||
name="ipc_port",
|
||||
description='internal ttl port(only usefull for multiple local instances)',
|
||||
default=5561
|
||||
)
|
||||
outliner_filter: bpy.props.StringProperty(name="None")
|
||||
is_admin: bpy.props.BoolProperty(
|
||||
name="is_admin",
|
||||
default=False
|
||||
)
|
||||
init_scene: bpy.props.BoolProperty(
|
||||
name="init_scene",
|
||||
default=True
|
||||
)
|
||||
start_empty: bpy.props.BoolProperty(
|
||||
name="start_empty",
|
||||
default=True
|
||||
)
|
||||
active_object: bpy.props.PointerProperty(
|
||||
name="active_object", type=bpy.types.Object)
|
||||
session_mode: bpy.props.EnumProperty(
|
||||
name='session_mode',
|
||||
description='session mode',
|
||||
@ -179,10 +186,11 @@ class SessionProps(bpy.types.PropertyGroup):
|
||||
description='Show only owned datablocks',
|
||||
default=True
|
||||
)
|
||||
use_select_right: bpy.props.BoolProperty(
|
||||
name="Selection right",
|
||||
description='Change right on selection',
|
||||
default=True
|
||||
user_snap_running: bpy.props.BoolProperty(
|
||||
default=False
|
||||
)
|
||||
time_snap_running: bpy.props.BoolProperty(
|
||||
default=False
|
||||
)
|
||||
|
||||
def load(self):
|
||||
@ -239,12 +247,13 @@ class SessionProps(bpy.types.PropertyGroup):
|
||||
|
||||
|
||||
classes = (
|
||||
SessionUser,
|
||||
ReplicatedDatablock,
|
||||
SessionProps,
|
||||
|
||||
)
|
||||
|
||||
libs = os.path.dirname(os.path.abspath(__file__))+"\\libs\\replication"
|
||||
libs = os.path.dirname(os.path.abspath(__file__))+"\\libs\\replication\\replication"
|
||||
|
||||
@persistent
|
||||
def load_handler(dummy):
|
||||
@ -267,7 +276,10 @@ def register():
|
||||
bpy.types.WindowManager.session = bpy.props.PointerProperty(
|
||||
type=SessionProps)
|
||||
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()
|
||||
|
||||
presence.register()
|
||||
|
@ -1,5 +1,4 @@
|
||||
__all__ = [
|
||||
'bl_user',
|
||||
'bl_object',
|
||||
'bl_mesh',
|
||||
'bl_camera',
|
||||
|
@ -1,5 +1,6 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
import copy
|
||||
|
||||
from .. import utils
|
||||
from .bl_datablock import BlDatablock
|
||||
@ -7,32 +8,109 @@ from .bl_datablock import BlDatablock
|
||||
# WIP
|
||||
|
||||
class BlAction(BlDatablock):
|
||||
def load(self, data, target):
|
||||
utils.dump_anything.load(target, data)
|
||||
bl_id = "actions"
|
||||
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):
|
||||
return bpy.data.actions.new(data["name"])
|
||||
|
||||
def load(self, data, target):
|
||||
pass
|
||||
# # find target object
|
||||
# object_ = bpy.context.scene.objects.active
|
||||
# if object_ is None:
|
||||
# raise RuntimeError("Nothing is selected.")
|
||||
# if object_.mode != 'POSE': # object must be in pose mode
|
||||
# raise RuntimeError("Object must be in pose mode.")
|
||||
# if object_.animation_data.action is None:
|
||||
# raise RuntimeError("Object needs an active action.")
|
||||
begin_frame = 100000
|
||||
end_frame = -100000
|
||||
|
||||
for dumped_fcurve in data["fcurves"]:
|
||||
begin_frame = min(
|
||||
begin_frame,
|
||||
min(
|
||||
[begin_frame] + [dkp["co"][0] for dkp in dumped_fcurve["keyframe_points"]]
|
||||
)
|
||||
)
|
||||
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):
|
||||
assert(pointer)
|
||||
data = utils.dump_datablock(pointer, 1)
|
||||
|
||||
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"] = []
|
||||
dumper.depth = 2
|
||||
for fcurve in self.pointer.fcurves:
|
||||
fc = {
|
||||
"data_path": fcurve.data_path,
|
||||
@ -49,20 +127,7 @@ class BlAction(BlDatablock):
|
||||
|
||||
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):
|
||||
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'
|
||||
|
||||
|
@ -3,28 +3,46 @@ import mathutils
|
||||
|
||||
from ..libs.overrider import Overrider
|
||||
from .. import utils
|
||||
from .. import presence
|
||||
from .. import presence, operators
|
||||
from .bl_datablock import BlDatablock
|
||||
|
||||
# WIP
|
||||
|
||||
|
||||
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):
|
||||
return bpy.data.armatures.new(data["name"])
|
||||
|
||||
def load(self, data, target):
|
||||
def load_implementation(self, data, target):
|
||||
# Load parent object
|
||||
if data['user'] not in bpy.data.objects.keys():
|
||||
parent_object = bpy.data.objects.new(data['user'], self.pointer)
|
||||
else:
|
||||
parent_object = bpy.data.objects[data['user']]
|
||||
parent_object = utils.find_from_attr(
|
||||
'uuid',
|
||||
data['user'],
|
||||
bpy.data.objects
|
||||
)
|
||||
|
||||
is_object_in_master = (data['user_collection'][0] == "Master Collection")
|
||||
#TODO: recursive parent collection loading
|
||||
if parent_object is None:
|
||||
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
|
||||
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():
|
||||
parent_collection = bpy.data.collections.new(data['user_collection'][0])
|
||||
parent_collection = bpy.data.collections.new(
|
||||
data['user_collection'][0])
|
||||
else:
|
||||
parent_collection = bpy.data.collections[data['user_collection'][0]]
|
||||
|
||||
@ -33,74 +51,81 @@ class BlArmature(BlDatablock):
|
||||
|
||||
# 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:
|
||||
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()
|
||||
|
||||
bpy.data.scenes[data['user_scene'][0]
|
||||
].collection. children.link(parent_collection)
|
||||
|
||||
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
|
||||
# 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')
|
||||
|
||||
for bone in data['bones']:
|
||||
if bone not in self.pointer.edit_bones:
|
||||
new_bone = self.pointer.edit_bones.new(bone)
|
||||
else:
|
||||
new_bone = self.pointer.edit_bones[bone]
|
||||
|
||||
new_bone.tail = data['bones'][bone]['tail_local']
|
||||
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']
|
||||
bone_data = data['bones'].get(bone)
|
||||
|
||||
if 'parent' in data['bones'][bone]:
|
||||
new_bone.parent = self.pointer.edit_bones[data['bones'][bone]['parent']['name']]
|
||||
new_bone.use_connect = data['bones'][bone]['use_connect']
|
||||
new_bone.tail = bone_data['tail_local']
|
||||
new_bone.head = bone_data['head_local']
|
||||
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.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
|
||||
# bpy_.selected_objects = [armature]
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
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]
|
||||
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)
|
||||
data['user_collection'] = [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)]
|
||||
data['user_collection'] = [
|
||||
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
|
||||
|
||||
def resolve(self):
|
||||
assert(self.data)
|
||||
self.pointer = bpy.data.armatures.get(self.data['name'])
|
||||
|
||||
def diff(self):
|
||||
False
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
@ -6,6 +6,13 @@ from .bl_datablock import 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):
|
||||
utils.dump_anything.load(target, data)
|
||||
|
||||
@ -18,7 +25,7 @@ class BlCamera(BlDatablock):
|
||||
def construct(self, data):
|
||||
return bpy.data.cameras.new(data["name"])
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
|
||||
dumper = utils.dump_anything.Dumper()
|
||||
@ -45,17 +52,5 @@ class BlCamera(BlDatablock):
|
||||
]
|
||||
return dumper.dump(pointer)
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.cameras)
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
||||
|
@ -6,6 +6,13 @@ from .bl_datablock import 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):
|
||||
if self.is_library:
|
||||
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"]:
|
||||
target.children.unlink(collection)
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
data = {}
|
||||
data['name'] = pointer.name
|
||||
@ -68,19 +75,8 @@ class BlCollection(BlDatablock):
|
||||
|
||||
data['children'] = collection_children
|
||||
|
||||
# dumper = utils.dump_anything.Dumper()
|
||||
# dumper.depth = 2
|
||||
# dumper.include_filter = ['name','objects', 'children']
|
||||
|
||||
# return dumper.dump(pointer)
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr(
|
||||
'uuid',
|
||||
self.uuid,
|
||||
bpy.data.collections)
|
||||
|
||||
def resolve_dependencies(self):
|
||||
deps = []
|
||||
|
||||
@ -94,11 +90,3 @@ class BlCollection(BlDatablock):
|
||||
def is_valid(self):
|
||||
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
|
||||
|
@ -5,6 +5,13 @@ from .. import utils
|
||||
from .bl_datablock import 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):
|
||||
return bpy.data.curves.new(data["name"], 'CURVE')
|
||||
|
||||
@ -29,7 +36,7 @@ class BlCurve(BlDatablock):
|
||||
utils.dump_anything.load(
|
||||
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)
|
||||
data = utils.dump_datablock(pointer, 1)
|
||||
data['splines'] = {}
|
||||
@ -52,15 +59,5 @@ class BlCurve(BlDatablock):
|
||||
data['type'] = 'CURVE'
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.curves)
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
@ -4,9 +4,65 @@ import mathutils
|
||||
from .. import utils
|
||||
from ..libs.replication.replication.data import ReplicatedDatablock
|
||||
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):
|
||||
"""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):
|
||||
super().__init__(*args, **kwargs)
|
||||
pointer = kwargs.get('pointer', None)
|
||||
@ -25,6 +81,8 @@ class BlDatablock(ReplicatedDatablock):
|
||||
if self.pointer and hasattr(self.pointer, 'uuid'):
|
||||
self.pointer.uuid = self.uuid
|
||||
|
||||
self.diff_method = DIFF_BINARY
|
||||
|
||||
def library_apply(self):
|
||||
"""Apply stored data
|
||||
"""
|
||||
@ -50,10 +108,68 @@ class BlDatablock(ReplicatedDatablock):
|
||||
def resolve_dependencies_library(self):
|
||||
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):
|
||||
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)
|
||||
|
||||
return dependencies
|
||||
|
@ -13,7 +13,7 @@ def load_gpencil_layer(target=None, data=None, create=False):
|
||||
|
||||
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])
|
||||
for stroke in data["frames"][frame]["strokes"]:
|
||||
@ -34,6 +34,13 @@ def load_gpencil_layer(target=None, data=None, create=False):
|
||||
|
||||
|
||||
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):
|
||||
return bpy.data.grease_pencils.new(data["name"])
|
||||
|
||||
@ -57,16 +64,13 @@ class BlGpencil(BlDatablock):
|
||||
for mat in data['materials']:
|
||||
target.materials.append(bpy.data.materials[mat])
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
data = utils.dump_datablock(pointer, 2)
|
||||
utils.dump_datablock_attibutes(
|
||||
pointer, ['layers'], 9, data)
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.grease_pencils)
|
||||
|
||||
def resolve_dependencies(self):
|
||||
deps = []
|
||||
|
||||
@ -77,11 +81,3 @@ class BlGpencil(BlDatablock):
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
@ -15,8 +15,11 @@ def dump_image(image):
|
||||
image.save()
|
||||
|
||||
if image.source == "FILE":
|
||||
image_path = bpy.path.abspath(image.filepath_raw)
|
||||
image_directory = os.path.dirname(image_path)
|
||||
os.makedirs(image_directory, exist_ok=True)
|
||||
image.save()
|
||||
file = open(image.filepath_raw, "rb")
|
||||
file = open(image_path, "rb")
|
||||
pixels = file.read()
|
||||
file.close()
|
||||
else:
|
||||
@ -24,6 +27,13 @@ def dump_image(image):
|
||||
return pixels
|
||||
|
||||
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):
|
||||
return bpy.data.images.new(
|
||||
name=data['name'],
|
||||
@ -44,32 +54,30 @@ class BlImage(BlDatablock):
|
||||
|
||||
image.source = 'FILE'
|
||||
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)
|
||||
data = {}
|
||||
data['pixels'] = dump_image(pointer)
|
||||
utils.dump_datablock_attibutes(pointer, [], 2, data)
|
||||
data = utils.dump_datablock_attibutes(
|
||||
pointer,
|
||||
["name", 'size', 'height', 'alpha', 'float_buffer', 'filepath', 'source'],
|
||||
2,
|
||||
data)
|
||||
return data
|
||||
dumper = utils.dump_anything.Dumper()
|
||||
dumper.depth = 2
|
||||
dumper.include_filter = [
|
||||
"name",
|
||||
'size',
|
||||
'height',
|
||||
'alpha',
|
||||
'float_buffer',
|
||||
'filepath',
|
||||
'source',
|
||||
'colorspace_settings']
|
||||
data.update(dumper.dump(pointer))
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.images)
|
||||
return data
|
||||
|
||||
def diff(self):
|
||||
return False
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
@ -6,6 +6,13 @@ from .bl_datablock import 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):
|
||||
utils.dump_anything.load(target, data)
|
||||
|
||||
@ -38,17 +45,8 @@ class BlLattice(BlDatablock):
|
||||
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lattices)
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
||||
|
||||
|
@ -6,6 +6,13 @@ from .bl_datablock import 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):
|
||||
with bpy.data.libraries.load(filepath=data["filepath"], link=True) as (sourceData, targetData):
|
||||
targetData = sourceData
|
||||
@ -17,16 +24,5 @@ class BlLibrary(BlDatablock):
|
||||
assert(pointer)
|
||||
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):
|
||||
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'
|
@ -6,13 +6,20 @@ from .bl_datablock import 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):
|
||||
return bpy.data.lights.new(data["name"], data["type"])
|
||||
|
||||
def load(self, data, target):
|
||||
utils.dump_anything.load(target, data)
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
dumper = utils.dump_anything.Dumper()
|
||||
dumper.depth = 3
|
||||
@ -39,16 +46,6 @@ class BlLight(BlDatablock):
|
||||
data = dumper.dump(pointer)
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lights)
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
@ -1,24 +1,44 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
import logging
|
||||
|
||||
from .. import utils
|
||||
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):
|
||||
utils.dump_anything.load(target, 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):
|
||||
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.depth = 1
|
||||
dumper.include_filter = [
|
||||
"name",
|
||||
'type',
|
||||
'influence_type',
|
||||
'influence_distance',
|
||||
'falloff',
|
||||
@ -29,21 +49,15 @@ class BlLightProbe(BlDatablock):
|
||||
'use_custom_parallax',
|
||||
'parallax_type',
|
||||
'parallax_distance',
|
||||
'grid_resolution_x',
|
||||
'grid_resolution_y',
|
||||
'grid_resolution_z',
|
||||
'visibility_buffer_bias',
|
||||
'visibility_bleed_bias',
|
||||
'visibility_blur'
|
||||
]
|
||||
|
||||
return dumper.dump(pointer)
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lattices)
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
||||
|
@ -68,10 +68,17 @@ def load_link(target_node_tree, source):
|
||||
|
||||
|
||||
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):
|
||||
return bpy.data.materials.new(data["name"])
|
||||
|
||||
def load(self, data, target):
|
||||
def load_implementation(self, data, target):
|
||||
target.name = data['name']
|
||||
if data['is_grease_pencil']:
|
||||
if not target.is_grease_pencil:
|
||||
@ -88,6 +95,8 @@ class BlMaterial(BlDatablock):
|
||||
|
||||
target.node_tree.nodes.clear()
|
||||
|
||||
utils.dump_anything.load(target,data)
|
||||
|
||||
# Load nodes
|
||||
for node in data["node_tree"]["nodes"]:
|
||||
load_node(target.node_tree, data["node_tree"]["nodes"][node])
|
||||
@ -98,7 +107,7 @@ class BlMaterial(BlDatablock):
|
||||
for link in data["node_tree"]["links"]:
|
||||
load_link(target.node_tree, data["node_tree"]["links"][link])
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
mat_dumper = utils.dump_anything.Dumper()
|
||||
mat_dumper.depth = 2
|
||||
@ -115,6 +124,7 @@ class BlMaterial(BlDatablock):
|
||||
node_dumper.depth = 1
|
||||
node_dumper.exclude_filter = [
|
||||
"dimensions",
|
||||
"show_expanded"
|
||||
"select",
|
||||
"bl_height_min",
|
||||
"bl_height_max",
|
||||
@ -133,7 +143,12 @@ class BlMaterial(BlDatablock):
|
||||
input_dumper.include_filter = ["default_value"]
|
||||
links_dumper = utils.dump_anything.Dumper()
|
||||
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)
|
||||
|
||||
if pointer.use_nodes:
|
||||
@ -175,9 +190,6 @@ class BlMaterial(BlDatablock):
|
||||
utils.dump_datablock_attibutes(pointer, ["grease_pencil"], 3, data)
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.materials)
|
||||
|
||||
def resolve_dependencies(self):
|
||||
# TODO: resolve node group deps
|
||||
deps = []
|
||||
@ -194,11 +206,3 @@ class BlMaterial(BlDatablock):
|
||||
def is_valid(self):
|
||||
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'
|
||||
|
@ -3,14 +3,17 @@ import bmesh
|
||||
import mathutils
|
||||
|
||||
from .. import utils
|
||||
from ..libs.replication.replication.constants import DIFF_BINARY
|
||||
from .bl_datablock import BlDatablock
|
||||
|
||||
|
||||
def dump_mesh(mesh, data={}):
|
||||
import bmesh
|
||||
|
||||
mesh_data = data
|
||||
mesh_buffer = bmesh.new()
|
||||
|
||||
# https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html
|
||||
mesh_buffer.from_mesh(mesh)
|
||||
|
||||
uv_layer = mesh_buffer.loops.layers.uv.verify()
|
||||
@ -72,20 +75,25 @@ def dump_mesh(mesh, data={}):
|
||||
uv_layers.append(uv_layer.name)
|
||||
|
||||
mesh_data["uv_layers"] = uv_layers
|
||||
return mesh_data
|
||||
# return mesh_data
|
||||
|
||||
|
||||
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):
|
||||
instance = bpy.data.meshes.new(data["name"])
|
||||
instance.uuid = self.uuid
|
||||
return instance
|
||||
|
||||
def load(self, data, target):
|
||||
def load_implementation(self, data, target):
|
||||
if not target or not target.is_editmode:
|
||||
# 1 - LOAD MATERIAL SLOTS
|
||||
material_to_load = []
|
||||
material_to_load = utils.revers(data["materials"])
|
||||
target.materials.clear()
|
||||
# SLots
|
||||
i = 0
|
||||
|
||||
@ -106,6 +114,8 @@ class BlMesh(BlDatablock):
|
||||
v2 = data["edges"][i]["verts"][1]
|
||||
edge = mesh_buffer.edges.new([verts[v1], verts[v2]])
|
||||
edge.smooth = data["edges"][i]["smooth"]
|
||||
|
||||
mesh_buffer.edges.ensure_lookup_table()
|
||||
for p in data["faces"]:
|
||||
verts = []
|
||||
for v in data["faces"][p]["verts"]:
|
||||
@ -129,21 +139,25 @@ class BlMesh(BlDatablock):
|
||||
|
||||
# 3 - LOAD METADATA
|
||||
# uv's
|
||||
for uv_layer in data['uv_layers']:
|
||||
target.uv_layers.new(name=uv_layer)
|
||||
utils.dump_anything.load(target.uv_layers, data['uv_layers'])
|
||||
|
||||
bevel_layer = mesh_buffer.verts.layers.bevel_weight.verify()
|
||||
skin_layer = mesh_buffer.verts.layers.skin.verify()
|
||||
|
||||
utils.dump_anything.load(target, data)
|
||||
|
||||
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
|
||||
data = utils.dump_datablock(pointer, 2)
|
||||
data = dump_mesh(pointer, data)
|
||||
dumper = utils.dump_anything.Dumper()
|
||||
dumper.depth = 2
|
||||
dumper.include_filter = [
|
||||
'name',
|
||||
'use_auto_smooth',
|
||||
'auto_smooth_angle'
|
||||
]
|
||||
data = dumper.dump(pointer)
|
||||
dump_mesh(pointer, data)
|
||||
# Fix material index
|
||||
m_list = []
|
||||
for material in pointer.materials:
|
||||
@ -154,9 +168,6 @@ class BlMesh(BlDatablock):
|
||||
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.meshes)
|
||||
|
||||
def resolve_dependencies(self):
|
||||
deps = []
|
||||
|
||||
@ -168,11 +179,3 @@ class BlMesh(BlDatablock):
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
||||
|
@ -6,6 +6,13 @@ from .bl_datablock import 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):
|
||||
return bpy.data.metaballs.new(data["name"])
|
||||
|
||||
@ -17,7 +24,7 @@ class BlMetaball(BlDatablock):
|
||||
new_element = target.elements.new(type=data["elements"][element]['type'])
|
||||
utils.dump_anything.load(new_element, data["elements"][element])
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
dumper = utils.dump_anything.Dumper()
|
||||
dumper.depth = 3
|
||||
@ -26,16 +33,5 @@ class BlMetaball(BlDatablock):
|
||||
data = dumper.dump(pointer)
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.metaballs)
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
@ -1,11 +1,43 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
import logging
|
||||
|
||||
from .. import utils
|
||||
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):
|
||||
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):
|
||||
pointer = None
|
||||
|
||||
@ -42,24 +74,31 @@ class BlObject(BlDatablock):
|
||||
elif data["data"] in bpy.data.speakers.keys():
|
||||
pointer = bpy.data.speakers[data["data"]]
|
||||
elif data["data"] in bpy.data.lightprobes.keys():
|
||||
pass
|
||||
# bpy need to support correct lightprobe creation from python
|
||||
# pointer = bpy.data.lightprobes[data["data"]]
|
||||
|
||||
# Only supported since 2.83
|
||||
if bpy.app.version[1] >= 83:
|
||||
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.uuid = self.uuid
|
||||
|
||||
return instance
|
||||
|
||||
def load(self, data, target):
|
||||
target.matrix_world = mathutils.Matrix(data["matrix_world"])
|
||||
def load_implementation(self, data, target):
|
||||
# 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"]
|
||||
# Load modifiers
|
||||
if hasattr(target, 'modifiers'):
|
||||
for local_modifier in target.modifiers:
|
||||
if local_modifier.name not in data['modifiers']:
|
||||
target.modifiers.remove(local_modifier)
|
||||
# TODO: smarter selective update
|
||||
target.modifiers.clear()
|
||||
|
||||
for modifier in data['modifiers']:
|
||||
target_modifier = target.modifiers.get(modifier)
|
||||
|
||||
@ -70,6 +109,40 @@ class BlObject(BlDatablock):
|
||||
utils.dump_anything.load(
|
||||
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
|
||||
if 'children' in data.keys():
|
||||
for child in data['children']:
|
||||
@ -84,13 +157,43 @@ class BlObject(BlDatablock):
|
||||
if data['instance_type'] == '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)
|
||||
dumper = utils.dump_anything.Dumper()
|
||||
dumper.depth = 1
|
||||
dumper.include_filter = [
|
||||
"name",
|
||||
"matrix_world",
|
||||
"rotation_mode",
|
||||
"parent",
|
||||
"data",
|
||||
@ -99,8 +202,12 @@ class BlObject(BlDatablock):
|
||||
"empty_display_type",
|
||||
"empty_display_size",
|
||||
"instance_collection",
|
||||
"instance_type"
|
||||
"instance_type",
|
||||
"location",
|
||||
"scale",
|
||||
'rotation_quaternion' if pointer.rotation_mode == 'QUATERNION' else 'rotation_euler',
|
||||
]
|
||||
|
||||
data = dumper.dump(pointer)
|
||||
|
||||
if self.is_library:
|
||||
@ -109,8 +216,55 @@ class BlObject(BlDatablock):
|
||||
# MODIFIERS
|
||||
if hasattr(pointer, 'modifiers'):
|
||||
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
|
||||
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
|
||||
if len(pointer.children) > 0:
|
||||
@ -120,13 +274,62 @@ class BlObject(BlDatablock):
|
||||
|
||||
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
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.objects)
|
||||
|
||||
def resolve_dependencies(self):
|
||||
deps = []
|
||||
deps = super().resolve_dependencies()
|
||||
|
||||
# Avoid Empty case
|
||||
if self.pointer.data:
|
||||
@ -138,19 +341,10 @@ class BlObject(BlDatablock):
|
||||
deps.append(self.pointer.library)
|
||||
|
||||
if self.pointer.instance_type == 'COLLECTION':
|
||||
#TODO: uuid based
|
||||
# TODO: uuid based
|
||||
deps.append(self.pointer.instance_collection)
|
||||
|
||||
return deps
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
||||
|
@ -5,6 +5,13 @@ from .. import utils
|
||||
from .bl_datablock import 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):
|
||||
instance = bpy.data.scenes.new(data["name"])
|
||||
instance.uuid = self.uuid
|
||||
@ -42,7 +49,7 @@ class BlScene(BlDatablock):
|
||||
if 'grease_pencil' in data.keys():
|
||||
target.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']]
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
data = {}
|
||||
|
||||
@ -58,12 +65,6 @@ class BlScene(BlDatablock):
|
||||
|
||||
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):
|
||||
deps = []
|
||||
|
||||
@ -87,10 +88,3 @@ class BlScene(BlDatablock):
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
@ -6,6 +6,13 @@ from .bl_datablock import 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):
|
||||
utils.dump_anything.load(target, data)
|
||||
|
||||
@ -34,17 +41,6 @@ class BlSpeaker(BlDatablock):
|
||||
|
||||
return dumper.dump(pointer)
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.lattices)
|
||||
|
||||
def is_valid(self):
|
||||
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'
|
||||
|
@ -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'
|
@ -7,6 +7,13 @@ from .bl_material import load_link, load_node
|
||||
|
||||
|
||||
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):
|
||||
return bpy.data.worlds.new(data["name"])
|
||||
|
||||
@ -26,7 +33,7 @@ class BlWorld(BlDatablock):
|
||||
for link in data["node_tree"]["links"]:
|
||||
load_link(target.node_tree, data["node_tree"]["links"][link])
|
||||
|
||||
def dump(self, pointer=None):
|
||||
def dump_implementation(self, data, pointer=None):
|
||||
assert(pointer)
|
||||
|
||||
world_dumper = utils.dump_anything.Dumper()
|
||||
@ -83,9 +90,6 @@ class BlWorld(BlDatablock):
|
||||
pointer.node_tree, ["links"], 3, data['node_tree'])
|
||||
return data
|
||||
|
||||
def resolve(self):
|
||||
self.pointer = utils.find_from_attr('uuid', self.uuid, bpy.data.worlds)
|
||||
|
||||
def resolve_dependencies(self):
|
||||
deps = []
|
||||
|
||||
@ -100,11 +104,3 @@ class BlWorld(BlDatablock):
|
||||
def is_valid(self):
|
||||
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'
|
||||
|
@ -3,10 +3,10 @@ import logging
|
||||
import bpy
|
||||
|
||||
from . import operators, presence, utils
|
||||
from .bl_types.bl_user import BlUser
|
||||
from .libs.replication.replication.constants import FETCHED, RP_COMMON
|
||||
from .libs.replication.replication.constants import FETCHED, RP_COMMON, STATE_INITIAL,STATE_QUITTING, STATE_ACTIVE, STATE_SYNCING, STATE_SRV_SYNC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
|
||||
class Delayable():
|
||||
@ -64,83 +64,113 @@ class ApplyTimer(Timer):
|
||||
super().__init__(timout)
|
||||
|
||||
def execute(self):
|
||||
if operators.client:
|
||||
nodes = operators.client.list(filter=self._type)
|
||||
client = operators.client
|
||||
if client and client.state['STATE'] == STATE_ACTIVE:
|
||||
nodes = client.list(filter=self._type)
|
||||
|
||||
for node in nodes:
|
||||
node_ref = operators.client.get(uuid=node)
|
||||
node_ref = client.get(uuid=node)
|
||||
|
||||
if node_ref.state == FETCHED:
|
||||
try:
|
||||
operators.client.apply(node)
|
||||
client.apply(node)
|
||||
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):
|
||||
def __init__(self, timout=.1):
|
||||
super().__init__(timout)
|
||||
self.last_selection = []
|
||||
self._last_selection = []
|
||||
self._user = None
|
||||
self._right_strategy = RP_COMMON
|
||||
|
||||
def execute(self):
|
||||
if operators.client:
|
||||
users = operators.client.list(filter=BlUser)
|
||||
|
||||
for user in users:
|
||||
user_ref = operators.client.get(uuid=user)
|
||||
session = operators.client
|
||||
settings = bpy.context.window_manager.session
|
||||
|
||||
# Local user
|
||||
if user_ref.pointer:
|
||||
current_selection = utils.get_selected_objects(
|
||||
bpy.context.scene)
|
||||
if current_selection != self.last_selection:
|
||||
right_strategy = operators.client.get_config()[
|
||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||
# Find user
|
||||
if self._user is None:
|
||||
self._user = session.online_users.get(settings.username)
|
||||
|
||||
if self._right_strategy is None:
|
||||
self._right_strategy = session.config[
|
||||
'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 = [
|
||||
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 = [
|
||||
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
|
||||
for obj in obj_ours:
|
||||
node = operators.client.get(uuid=obj)
|
||||
node = session.get(uuid=obj)
|
||||
|
||||
if node and node.owner == RP_COMMON:
|
||||
recursive = True
|
||||
if node.data and 'instance_type' in node.data.keys():
|
||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||
|
||||
operators.client.change_owner(
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
settings.username,
|
||||
recursive=recursive)
|
||||
else:
|
||||
return
|
||||
|
||||
self.last_selection = current_selection
|
||||
user_ref.pointer.update_selected_objects(
|
||||
bpy.context)
|
||||
user_ref.update()
|
||||
self._last_selection = current_selection
|
||||
|
||||
# change old selection right to common
|
||||
for obj in obj_common:
|
||||
node = operators.client.get(uuid=obj)
|
||||
user_metadata = {
|
||||
'selected_objects': current_selection
|
||||
}
|
||||
|
||||
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'
|
||||
operators.client.change_owner(
|
||||
node.uuid,
|
||||
session.update_user_metadata(user_metadata)
|
||||
logger.info("Update selection")
|
||||
|
||||
# Fix deselection until right managment refactoring (with Roles concepts)
|
||||
if len(current_selection) == 0 and self._right_strategy == RP_COMMON:
|
||||
owned_keys = session.list(
|
||||
filter_owner=settings.username)
|
||||
for key in owned_keys:
|
||||
node = session.get(uuid=key)
|
||||
|
||||
session.change_owner(
|
||||
key,
|
||||
RP_COMMON,
|
||||
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:
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@ -160,37 +190,103 @@ class Draw(Delayable):
|
||||
bpy.types.SpaceView3D.draw_handler_remove(
|
||||
self._handler, "WINDOW")
|
||||
except:
|
||||
logger.error("draw already unregistered")
|
||||
pass
|
||||
|
||||
|
||||
class DrawClient(Draw):
|
||||
def execute(self):
|
||||
repo = operators.client
|
||||
if repo and presence.renderer:
|
||||
settings = bpy.context.window_manager.session
|
||||
client_list = [key for key in repo.list(filter=BlUser) if
|
||||
key != settings.user_uuid]
|
||||
session = getattr(operators, 'client', None)
|
||||
renderer = getattr(presence, 'renderer', None)
|
||||
|
||||
for cli in client_list:
|
||||
cli_ref = repo.get(uuid=cli)
|
||||
if cli_ref.data.get('name'):
|
||||
if settings.presence_show_selected:
|
||||
presence.renderer.draw_client_selection(
|
||||
cli_ref.data['name'], cli_ref.data['color'], cli_ref.data['selected_objects'])
|
||||
if settings.presence_show_user:
|
||||
presence.renderer.draw_client_camera(
|
||||
cli_ref.data['name'], cli_ref.data['location'], cli_ref.data['color'])
|
||||
if session and renderer and session.state['STATE'] == STATE_ACTIVE:
|
||||
settings = bpy.context.window_manager.session
|
||||
users = session.online_users
|
||||
|
||||
for user in users.values():
|
||||
metadata = user.get('metadata')
|
||||
|
||||
if 'color' in metadata:
|
||||
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):
|
||||
def __init__(self, timout=1, client_uuid=None):
|
||||
assert(client_uuid)
|
||||
self._client_uuid = client_uuid
|
||||
def __init__(self, timout=.5):
|
||||
super().__init__(timout)
|
||||
self.handle_quit = False
|
||||
|
||||
def execute(self):
|
||||
if self._client_uuid and operators.client:
|
||||
client = operators.client.get(uuid=self._client_uuid)
|
||||
settings = bpy.context.window_manager.session
|
||||
session_info = bpy.context.window_manager.session
|
||||
session = getattr(operators, 'client', None)
|
||||
renderer = getattr(presence, 'renderer', None)
|
||||
|
||||
if client:
|
||||
client.pointer.update_location()
|
||||
if session and renderer and session.state['STATE'] == STATE_ACTIVE:
|
||||
# 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()
|
@ -6,7 +6,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
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 = os.path.join(CONFIG_DIR, "app.yaml")
|
||||
@ -48,12 +48,6 @@ def module_can_be_imported(name):
|
||||
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():
|
||||
# pip can not necessarily be imported into Blender after this
|
||||
get_pip_path = Path(__file__).parent / "libs" / "get-pip.py"
|
||||
@ -61,10 +55,8 @@ def install_pip():
|
||||
|
||||
|
||||
def install_package(name):
|
||||
target = get_package_install_directory()
|
||||
|
||||
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install",
|
||||
name, '--target', target], cwd=SUBPROCESS_DIR)
|
||||
name], cwd=SUBPROCESS_DIR)
|
||||
|
||||
def check_dir(dir):
|
||||
if not os.path.exists(dir):
|
||||
|
@ -92,11 +92,13 @@ class Dumper:
|
||||
|
||||
def _build_inline_dump_functions(self):
|
||||
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_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_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_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_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_float = (_dump_filter_type(float), 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_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_matrix = (_dump_filter_type(mathutils.Matrix), self._dump_matrix)
|
||||
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_default = (_dump_filter_default, self._dump_default)
|
||||
|
||||
@ -136,9 +141,18 @@ class Dumper:
|
||||
def _dump_vector_as_leaf(self, vector, depth):
|
||||
return list(vector)
|
||||
|
||||
def _dump_quaternion_as_leaf(self, quaternion, depth):
|
||||
return list(quaternion)
|
||||
|
||||
def _dump_color_as_leaf(self, color, depth):
|
||||
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 is_valid_property(p):
|
||||
try:
|
||||
@ -173,12 +187,14 @@ class Dumper:
|
||||
self._match_type_int,
|
||||
self._match_type_float,
|
||||
self._match_type_string,
|
||||
self._match_type_ref,
|
||||
self._match_type_ID,
|
||||
self._match_type_bpy_prop_collection,
|
||||
self._match_type_array,
|
||||
self._match_type_matrix,
|
||||
self._match_type_vector,
|
||||
self._match_type_color,
|
||||
self._match_type_quaternion,
|
||||
self._match_type_euler,
|
||||
self._match_type_color,
|
||||
self._match_default
|
||||
]
|
||||
@ -307,6 +323,12 @@ class Loader:
|
||||
def _load_vector(self, 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):
|
||||
ordered_keys = []
|
||||
for order_element in self.order:
|
||||
@ -336,6 +358,8 @@ class Loader:
|
||||
(_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.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.StringProperty), self._load_identity),
|
||||
(_load_filter_type(T.EnumProperty), self._load_identity),
|
||||
|
@ -4,53 +4,51 @@ import os
|
||||
import queue
|
||||
import random
|
||||
import string
|
||||
import subprocess
|
||||
import time
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
|
||||
import msgpack
|
||||
from subprocess import PIPE, Popen, TimeoutExpired
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
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.exception import NonAuthorizedOperationError
|
||||
from .libs.replication.replication.interface import Session
|
||||
from .libs.replication.replication.constants import (
|
||||
STATE_ACTIVE,
|
||||
STATE_INITIAL,
|
||||
STATE_SYNCING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.ERROR)
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
client = None
|
||||
delayables = []
|
||||
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):
|
||||
global client
|
||||
|
||||
for type_id in supported_types_id:
|
||||
if hasattr(bpy.data, type_id):
|
||||
for item in getattr(bpy.data, type_id):
|
||||
if client.exist(uuid=item.uuid):
|
||||
for d in delayables:
|
||||
try:
|
||||
d.unregister()
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
client.add(item)
|
||||
|
||||
stop_modal_executor = True
|
||||
|
||||
# OPERATORS
|
||||
|
||||
|
||||
class SessionStartOperator(bpy.types.Operator):
|
||||
bl_idname = "session.start"
|
||||
bl_label = "start"
|
||||
bl_description = "connect to a net server"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
host: bpy.props.BoolProperty(default=False)
|
||||
|
||||
@ -59,94 +57,92 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client, delayables
|
||||
global client, delayables, ui_context, server_process
|
||||
settings = context.window_manager.session
|
||||
users = bpy.data.window_managers['WinMan'].online_users
|
||||
|
||||
# TODO: Sync server clients
|
||||
users.clear()
|
||||
delayables.clear()
|
||||
# save config
|
||||
settings.save(context)
|
||||
|
||||
bpy_factory = ReplicatedDataFactory()
|
||||
supported_bl_types = []
|
||||
ui_context = context.copy()
|
||||
|
||||
# init the factory with supported types
|
||||
for type in bl_types.types_to_register():
|
||||
_type = getattr(bl_types, type)
|
||||
supported_bl_types.append(_type.bl_id)
|
||||
type_module = getattr(bl_types, type)
|
||||
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
|
||||
type_local_config = settings.supported_datablock[_type.bl_rep_class.__name__]
|
||||
type_local_config = settings.supported_datablock[type_impl_name]
|
||||
|
||||
bpy_factory.register_type(
|
||||
_type.bl_class,
|
||||
_type.bl_rep_class,
|
||||
type_module_class.bl_class,
|
||||
type_module_class,
|
||||
timer=type_local_config.bl_delay_refresh,
|
||||
automatic=type_local_config.auto_push)
|
||||
|
||||
if type_local_config.bl_delay_apply > 0:
|
||||
delayables.append(delayable.ApplyTimer(
|
||||
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:
|
||||
# Scene setup
|
||||
if settings.start_empty:
|
||||
utils.clean_scene()
|
||||
|
||||
try:
|
||||
for scene in bpy.data.scenes:
|
||||
scene_uuid = client.add(scene)
|
||||
client.commit(scene_uuid)
|
||||
|
||||
client.host(
|
||||
id=settings.username,
|
||||
address=settings.ip,
|
||||
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
|
||||
|
||||
# Join a session
|
||||
else:
|
||||
utils.clean_scene()
|
||||
|
||||
try:
|
||||
client.connect(
|
||||
id=settings.username,
|
||||
address=settings.ip,
|
||||
port=settings.port
|
||||
port=settings.port,
|
||||
ipc_port=settings.ipc_port
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if client.state == 0:
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
logger.error(f"Error: {e}")
|
||||
finally:
|
||||
settings.is_admin = False
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"A session is already hosted on this address")
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
|
||||
# Init user settings
|
||||
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))
|
||||
# Background client updates service
|
||||
#TODO: Refactoring
|
||||
delayables.append(delayable.ClientUpdate())
|
||||
delayables.append(delayable.DrawClient())
|
||||
|
||||
delayables.append(delayable.DynamicRightSelectTimer())
|
||||
|
||||
# Push all added values
|
||||
client.push_all()
|
||||
|
||||
# Launch drawing module
|
||||
if settings.enable_presence:
|
||||
presence.renderer.run()
|
||||
@ -155,6 +151,10 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
for d in delayables:
|
||||
d.register()
|
||||
|
||||
global modal_executor_queue
|
||||
modal_executor_queue = queue.Queue()
|
||||
bpy.ops.session.apply_armature_operator()
|
||||
|
||||
self.report(
|
||||
{'INFO'},
|
||||
"connexion on tcp://{}:{}".format(settings.ip, settings.port))
|
||||
@ -172,20 +172,13 @@ class SessionStopOperator(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client, delayables
|
||||
settings = context.window_manager.session
|
||||
settings.is_admin = False
|
||||
global client, delayables, stop_modal_executor
|
||||
assert(client)
|
||||
|
||||
client.remove(settings.user_uuid)
|
||||
client.disconnect()
|
||||
|
||||
for d in delayables:
|
||||
try:
|
||||
d.unregister()
|
||||
except:
|
||||
continue
|
||||
presence.renderer.stop()
|
||||
client.disconnect()
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@ -264,6 +257,14 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
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)
|
||||
wm.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
@ -273,7 +274,9 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
||||
wm.event_timer_remove(self._timer)
|
||||
|
||||
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)
|
||||
return {'CANCELLED'}
|
||||
|
||||
@ -281,9 +284,64 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
||||
area, region, rv3d = presence.view3d_find()
|
||||
global client
|
||||
|
||||
target_client = client.get(uuid=self.target_client)
|
||||
if target_client:
|
||||
rv3d.view_matrix = mathutils.Matrix(target_client.data['view_matrix'])
|
||||
if client:
|
||||
target_ref = client.online_users.get(self.target_client)
|
||||
|
||||
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:
|
||||
return {"CANCELLED"}
|
||||
|
||||
@ -330,23 +388,130 @@ class SessionCommit(bpy.types.Operator):
|
||||
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 = (
|
||||
SessionStartOperator,
|
||||
SessionStopOperator,
|
||||
SessionPropertyRemoveOperator,
|
||||
SessionSnapUserOperator,
|
||||
SessionSnapTimeOperator,
|
||||
SessionPropertyRightOperator,
|
||||
SessionApply,
|
||||
SessionCommit,
|
||||
ApplyArmatureOperator,
|
||||
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_pre_handler(dummy):
|
||||
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()
|
||||
|
||||
|
||||
@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():
|
||||
from bpy.utils import register_class
|
||||
for cls in classes:
|
||||
@ -354,10 +519,18 @@ def register():
|
||||
|
||||
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():
|
||||
global client
|
||||
|
||||
if client and client.state == 2:
|
||||
if client and client.state['STATE'] == 2:
|
||||
client.disconnect()
|
||||
client = None
|
||||
|
||||
@ -367,5 +540,13 @@ def unregister():
|
||||
|
||||
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__":
|
||||
register()
|
||||
|
@ -31,7 +31,7 @@ def view3d_find():
|
||||
|
||||
def refresh_3d_view():
|
||||
area, region, rv3d = view3d_find()
|
||||
|
||||
if area and region and rv3d:
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
@ -69,7 +69,7 @@ def get_default_bbox(obj, radius):
|
||||
return [(point.x, point.y, point.z)
|
||||
for point in bbox_corners]
|
||||
|
||||
def get_client_cam_points():
|
||||
def get_view_corners():
|
||||
area, region, rv3d = view3d_find()
|
||||
|
||||
v1 = [0, 0, 0]
|
||||
@ -78,6 +78,7 @@ def get_client_cam_points():
|
||||
v4 = [0, 0, 0]
|
||||
v5 = [0, 0, 0]
|
||||
v6 = [0, 0, 0]
|
||||
v7 = [0, 0, 0]
|
||||
|
||||
if area and region and rv3d:
|
||||
width = region.width
|
||||
@ -112,31 +113,14 @@ def get_bb_coords_from_obj(object, parent=None):
|
||||
return [(point.x, point.y, point.z)
|
||||
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):
|
||||
current_coords = get_client_cam_points()
|
||||
def get_view_matrix():
|
||||
area, region, rv3d = view3d_find()
|
||||
|
||||
current_coords = list(get_client_cam_points())
|
||||
if current_coords and self.location != current_coords:
|
||||
self.location = current_coords
|
||||
|
||||
if area and region and rv3d:
|
||||
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):
|
||||
self.selected_objects = utils.get_selected_objects(context.scene)
|
||||
return matrix_dumper.dump(rv3d.view_matrix)
|
||||
|
||||
def update_presence(self, context):
|
||||
global renderer
|
||||
@ -171,8 +155,12 @@ class DrawFactory(object):
|
||||
self.register_handlers()
|
||||
|
||||
def stop(self):
|
||||
self.flush_users()
|
||||
self.flush_selection()
|
||||
self.unregister_handlers()
|
||||
|
||||
refresh_3d_view()
|
||||
|
||||
def register_handlers(self):
|
||||
self.draw3d_handle = bpy.types.SpaceView3D.draw_handler_add(
|
||||
self.draw3d_callback, (), 'WINDOW', 'POST_VIEW')
|
||||
@ -215,14 +203,14 @@ class DrawFactory(object):
|
||||
|
||||
self.d2d_items.clear()
|
||||
|
||||
def draw_client_selection(self, client_uuid, client_color, client_selection):
|
||||
local_user = bpy.context.window_manager.session.user_uuid
|
||||
def draw_client_selection(self, client_id, client_color, client_selection):
|
||||
local_user = bpy.context.window_manager.session.username
|
||||
|
||||
if local_user != client_uuid:
|
||||
self.flush_selection(client_uuid)
|
||||
if local_user != client_id:
|
||||
self.flush_selection(client_id)
|
||||
|
||||
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)
|
||||
if not ob:
|
||||
@ -271,11 +259,11 @@ class DrawFactory(object):
|
||||
|
||||
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:
|
||||
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:
|
||||
indices = (
|
||||
(1, 3), (2, 1), (3, 0),
|
||||
@ -290,8 +278,8 @@ class DrawFactory(object):
|
||||
batch = batch_for_shader(
|
||||
shader, 'LINES', {"pos": position}, indices=indices)
|
||||
|
||||
self.d3d_items[client_uuid] = (shader, batch, color)
|
||||
self.d2d_items[client_uuid] = (position[1], client_uuid, color)
|
||||
self.d3d_items[client_id] = (shader, batch, color)
|
||||
self.d2d_items[client_id] = (position[1], client_id, color)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Draw client exception {}".format(e))
|
||||
|
237
multi_user/ui.py
@ -1,9 +1,12 @@
|
||||
import bpy
|
||||
|
||||
from . import operators
|
||||
from .bl_types.bl_user import BlUser
|
||||
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
|
||||
'TRIA_UP', # COMMITED
|
||||
@ -12,6 +15,44 @@ ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
|
||||
'FILE_REFRESH', # UP
|
||||
'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):
|
||||
"""Settings panel"""
|
||||
@ -32,22 +73,55 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
if hasattr(context.window_manager, 'session'):
|
||||
# STATE INITIAL
|
||||
if not operators.client \
|
||||
or (operators.client and operators.client.state == 0):
|
||||
or (operators.client and operators.client.state['STATE'] == STATE_INITIAL):
|
||||
pass
|
||||
else:
|
||||
# STATE ACTIVE
|
||||
if operators.client.state == 2:
|
||||
cli_state = operators.client.state
|
||||
|
||||
row.label(text=f"Status : {get_state_str(cli_state['STATE'])}")
|
||||
row = layout.row()
|
||||
|
||||
current_state = cli_state['STATE']
|
||||
|
||||
# STATE ACTIVE
|
||||
if current_state == STATE_ACTIVE:
|
||||
row.operator("session.stop", icon='QUIT', text="Exit")
|
||||
row = layout.row()
|
||||
|
||||
# STATE SYNCING
|
||||
else:
|
||||
status = "connecting..."
|
||||
row.label(text=status)
|
||||
# CONNECTION STATE
|
||||
elif current_state in [
|
||||
STATE_SRV_SYNC,
|
||||
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.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):
|
||||
bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel"
|
||||
@ -60,7 +134,7 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
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):
|
||||
layout = self.layout
|
||||
@ -72,24 +146,24 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
||||
row.prop(settings, "session_mode", expand=True)
|
||||
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()
|
||||
|
||||
row = box.row()
|
||||
row.prop(settings, "ip", text="IP")
|
||||
row = box.row()
|
||||
row.label(text="Port:")
|
||||
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.operator("session.start", text="CONNECT").host = False
|
||||
|
||||
@ -105,7 +179,7 @@ class SESSION_PT_settings_user(bpy.types.Panel):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
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):
|
||||
layout = self.layout
|
||||
@ -133,7 +207,7 @@ class SESSION_PT_settings_replication(bpy.types.Panel):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
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):
|
||||
layout = self.layout
|
||||
@ -167,7 +241,7 @@ class SESSION_PT_settings_replication(bpy.types.Panel):
|
||||
|
||||
class SESSION_PT_user(bpy.types.Panel):
|
||||
bl_idname = "MULTIUSER_USER_PT_panel"
|
||||
bl_label = "Users"
|
||||
bl_label = "Online users"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Multiuser"
|
||||
@ -175,48 +249,62 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
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.
|
||||
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)
|
||||
if client_keys and len(client_keys) > 0:
|
||||
for key in client_keys:
|
||||
area_msg = col.row(align=True)
|
||||
item_box = area_msg.box()
|
||||
client = operators.client.get(uuid=key).data
|
||||
row = layout.row()
|
||||
layout.template_list("SESSION_UL_users", "", context.window_manager, "online_users", context.window_manager, "user_index")
|
||||
|
||||
info = ""
|
||||
|
||||
detail_item_row = item_box.row(align=True)
|
||||
|
||||
if client.get('name'):
|
||||
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(
|
||||
if active_user != 0 and active_user.username != settings.username:
|
||||
row = layout.row()
|
||||
user_operations = row.split()
|
||||
user_operations.alert = context.window_manager.session.time_snap_running
|
||||
user_operations.operator(
|
||||
"session.snapview",
|
||||
text="",
|
||||
icon='VIEW_CAMERA').target_client = key
|
||||
row = layout.row()
|
||||
else:
|
||||
row.label(text="Empty")
|
||||
icon='VIEW_CAMERA').target_client = active_user.username
|
||||
|
||||
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):
|
||||
@ -230,7 +318,8 @@ class SESSION_PT_presence(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
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")
|
||||
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):
|
||||
settings = context.window_manager.session
|
||||
item = operators.client.get(uuid=property_uuid)
|
||||
|
||||
if item.str_type == 'BlUser' or item.state == ERROR:
|
||||
if item.state == ERROR:
|
||||
return
|
||||
|
||||
area_msg = parent.row(align=True)
|
||||
@ -317,7 +434,7 @@ class SESSION_PT_outliner(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
|
||||
@ -365,13 +482,15 @@ class SESSION_PT_outliner(bpy.types.Panel):
|
||||
|
||||
|
||||
classes = (
|
||||
SESSION_UL_users,
|
||||
SESSION_PT_settings,
|
||||
SESSION_PT_settings_user,
|
||||
SESSION_PT_settings_network,
|
||||
SESSION_PT_presence,
|
||||
SESSION_PT_settings_replication,
|
||||
SESSION_PT_user,
|
||||
SESSION_PT_outliner
|
||||
SESSION_PT_outliner,
|
||||
SESSION_PT_services
|
||||
)
|
||||
|
||||
|
||||
|
@ -5,6 +5,7 @@ import random
|
||||
import string
|
||||
import sys
|
||||
from uuid import uuid4
|
||||
from collections.abc import Iterable
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
@ -13,7 +14,18 @@ from . import environment, presence
|
||||
from .libs import dump_anything
|
||||
|
||||
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):
|
||||
@ -45,7 +57,7 @@ def get_datablock_users(datablock):
|
||||
def random_string_digits(stringLength=6):
|
||||
"""Generate a random string of letters and 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():
|
||||
@ -92,8 +104,8 @@ def get_armature_edition_context(armature):
|
||||
return override
|
||||
|
||||
|
||||
def get_selected_objects(scene):
|
||||
return [obj.uuid for obj in scene.objects if obj.select_get()]
|
||||
def get_selected_objects(scene, active_view_layer):
|
||||
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
||||
|
||||
|
||||
def load_dict(src_dict, target):
|
||||
@ -139,3 +151,13 @@ def dump_datablock_attibutes(datablock=None, attributes=[], depth=1, dickt=None)
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
|