Merge branch 'develop' into feature/event_driven_updates
@ -4,5 +4,4 @@ stages:
|
|||||||
|
|
||||||
include:
|
include:
|
||||||
- local: .gitlab/ci/test.gitlab-ci.yml
|
- local: .gitlab/ci/test.gitlab-ci.yml
|
||||||
- local: .gitlab/ci/build.gitlab-ci.yml
|
- local: .gitlab/ci/build.gitlab-ci.yml
|
||||||
|
|
@ -1,10 +1,7 @@
|
|||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
image: python:latest
|
image: debian:stable-slim
|
||||||
script:
|
script:
|
||||||
- git submodule init
|
|
||||||
- git submodule update
|
|
||||||
- cd multi_user/libs/replication
|
|
||||||
- rm -rf tests .git .gitignore script
|
- rm -rf tests .git .gitignore script
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
@ -12,3 +9,4 @@ build:
|
|||||||
paths:
|
paths:
|
||||||
- multi_user
|
- multi_user
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
image: python:latest
|
image: python:3.7
|
||||||
script:
|
script:
|
||||||
- git submodule init
|
- git submodule init
|
||||||
- git submodule update
|
- git submodule update
|
||||||
- apt update
|
- apt-get update
|
||||||
# install blender to get all required dependencies
|
# install blender to get all required dependencies
|
||||||
# TODO: indtall only dependencies
|
# TODO: indtall only dependencies
|
||||||
- apt install -f -y blender
|
- apt install -f -y blender
|
||||||
- pip install blender-addon-tester
|
- python -m pip install blender-addon-tester
|
||||||
- python scripts/test_addon.py
|
- python scripts/test_addon.py
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,5 +42,5 @@ logs, and code as it's tough to read otherwise.)
|
|||||||
(If you can, link to the line of code that might be responsible for the problem)
|
(If you can, link to the line of code that might be responsible for the problem)
|
||||||
|
|
||||||
|
|
||||||
/label ~bug
|
/label ~type::bug
|
||||||
/cc @project-manager
|
/cc @project-manager
|
||||||
|
30
.gitlab/issue_templates/Documentation.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
### Problem to solve
|
||||||
|
|
||||||
|
<!-- Include the following detail as necessary:
|
||||||
|
* What feature(s) affected?
|
||||||
|
* What docs or doc section affected? Include links or paths.
|
||||||
|
* Is there a problem with a specific document, or a feature/process that's not addressed sufficiently in docs?
|
||||||
|
* Any other ideas or requests?
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Further details
|
||||||
|
|
||||||
|
<!--
|
||||||
|
* Any concepts, procedures, reference info we could add to make it easier to successfully use the multi-user addom?
|
||||||
|
* Include use cases, benefits, and/or goals for this work.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Proposal
|
||||||
|
|
||||||
|
<!-- Further specifics for how can we solve the problem. -->
|
||||||
|
|
||||||
|
### Who can address the issue
|
||||||
|
|
||||||
|
<!-- What if any special expertise is required to resolve this issue? -->
|
||||||
|
|
||||||
|
### Other links/references
|
||||||
|
|
||||||
|
<!-- E.g. related GitLab issues/MRs -->
|
||||||
|
|
||||||
|
/label ~type::documentation
|
||||||
|
/cc @project-manager
|
18
.gitlab/issue_templates/Feature Proposal.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
### Problem to solve
|
||||||
|
|
||||||
|
<!-- What problem do we solve? Try to define the who/what/why of the opportunity as a user story. For example, "As a (who), I want (what), so I can (why/value)." -->
|
||||||
|
|
||||||
|
|
||||||
|
### Proposal
|
||||||
|
|
||||||
|
<!-- How are we going to solve the problem?-->
|
||||||
|
|
||||||
|
### Further details
|
||||||
|
|
||||||
|
<!-- Include use cases, benefits, goals, or any other details that will help us understand the problem better. -->
|
||||||
|
|
||||||
|
|
||||||
|
### Links / references
|
||||||
|
|
||||||
|
/label ~type::feature request
|
||||||
|
/cc @project-manager
|
34
.gitlab/issue_templates/Refactoring.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please briefly describe what part of the code base needs to be refactored.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Explain the benefits of refactoring this code.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please list features that can break because of this refactoring and how you intend to solve that.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Involved components
|
||||||
|
|
||||||
|
<!--
|
||||||
|
List files or directories that will be changed by the refactoring.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Optional: Intended side effects
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If the refactoring involves changes apart from the main improvements (such as a better UI), list them here.
|
||||||
|
It may be a good idea to create separate issues and link them here.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
/label ~type::refactoring
|
||||||
|
/cc @project-manager
|
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "multi_user/libs/replication"]
|
|
||||||
path = multi_user/libs/replication
|
|
||||||
url = https://gitlab.com/slumber/replication.git
|
|
||||||
|
19
CHANGELOG.md
@ -42,11 +42,26 @@ All notable changes to this project will be documented in this file.
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Auto updater support
|
- Auto updater support
|
||||||
- Performances improvements on Meshes, Gpencils, Actions
|
- Big Performances improvements on Meshes, Gpencils, Actions
|
||||||
- Multi-scene workflow support
|
- Multi-scene workflow support
|
||||||
- Render setting synchronisation
|
- Render setting synchronization
|
||||||
- Kick command
|
- Kick command
|
||||||
|
- Dedicated server with a basic command set
|
||||||
|
- Administrator session status
|
||||||
|
- Tests
|
||||||
|
- Blender 2.83-2.90 support
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Config is now stored in blender user preference
|
- Config is now stored in blender user preference
|
||||||
|
- Documentation update
|
||||||
|
- Connection protocol
|
||||||
|
- UI revamp:
|
||||||
|
- user localization
|
||||||
|
- repository init
|
||||||
|
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Unused strict right management strategy
|
||||||
|
- Legacy config management system
|
@ -11,7 +11,7 @@ This tool aims to allow multiple users to work on the same scene over the networ
|
|||||||
|
|
||||||
## Quick installation
|
## Quick installation
|
||||||
|
|
||||||
1. Download latest release [multi_user.zip](/uploads/8aef79c7cf5b1d9606dc58307fd9ad8b/multi_user.zip).
|
1. Download latest release [multi_user.zip](https://gitlab.com/slumber/multi-user/-/jobs/artifacts/master/download?job=build).
|
||||||
2. Run blender as administrator (dependencies installation).
|
2. Run blender as administrator (dependencies installation).
|
||||||
3. Install last_version.zip from your addon preferences.
|
3. Install last_version.zip from your addon preferences.
|
||||||
|
|
||||||
|
BIN
docs/about/img/about_chain.gif
Normal file
After Width: | Height: | Size: 5.8 MiB |
@ -5,6 +5,11 @@ 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.
|
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.
|
||||||
|
|
||||||
|
.. figure:: img/about_chain.gif
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
The linear workflow problems
|
||||||
|
|
||||||
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 ?
|
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.
|
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.
|
||||||
|
59
docs/getting_started/glossary.rst
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
========
|
||||||
|
Glossary
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
.. glossary::
|
||||||
|
|
||||||
|
.. _admin:
|
||||||
|
|
||||||
|
administrator
|
||||||
|
|
||||||
|
*A session administrator can manage users (kick) and have a write access on
|
||||||
|
each datablock. He could also init a dedicated server repository.*
|
||||||
|
|
||||||
|
.. _session-status:
|
||||||
|
|
||||||
|
session status
|
||||||
|
|
||||||
|
*Located in the title of the multi-user panel, the session status show
|
||||||
|
you the connection state.*
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_session_status.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Session status in panel title bar
|
||||||
|
|
||||||
|
All possible state are listed here with their meaning:*
|
||||||
|
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| State | Description |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| WARMING UP DATA | Commiting local data |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| FETCHING | Dowloading snapshot from the server |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| AUTHENTIFICATION | Initial server authentication |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| ONLINE | Connected to the session |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| PUSHING | Init the server repository by pushing ours |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| INIT | Initial state |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| QUITTING | Exiting the session |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| LAUNCHING SERVICES | Launching local services. Services are spetialized daemons running in the background. ) |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
| LOBBY | The lobby is a waiting state triggered when the server repository hasn't been initiated yet |
|
||||||
|
| | |
|
||||||
|
| | Once initialized, the server will automatically launch all client in the **LOBBY**. |
|
||||||
|
+--------------------+---------------------------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
.. _common-right:
|
||||||
|
|
||||||
|
common right
|
||||||
|
|
||||||
|
When a data block is under common right, it is available for everyone to modification.
|
||||||
|
The rights will be given to the user that select it first.
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 22 KiB |
BIN
docs/getting_started/img/quickstart_session_init.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
docs/getting_started/img/quickstart_session_status.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/getting_started/img/quickstart_snap_time.gif
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/getting_started/img/quickstart_snap_view.gif
Normal file
After Width: | Height: | Size: 5.2 MiB |
BIN
docs/getting_started/img/quickstart_user_info.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 4.6 KiB |
BIN
docs/getting_started/img/quickstart_user_representation.png
Normal file
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 18 KiB |
@ -1,3 +1,4 @@
|
|||||||
|
===============
|
||||||
Getting started
|
Getting started
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@ -7,3 +8,4 @@ Getting started
|
|||||||
|
|
||||||
install
|
install
|
||||||
quickstart
|
quickstart
|
||||||
|
glossary
|
||||||
|
@ -2,8 +2,12 @@
|
|||||||
Installation
|
Installation
|
||||||
============
|
============
|
||||||
|
|
||||||
*The process is the same for linux, mac and windows.*
|
.. hint::
|
||||||
|
The process is the same for linux, mac and windows.
|
||||||
|
|
||||||
1. Download latest `release <https://gitlab.com/slumber/multi-user/-/jobs/artifacts/master/download?job=build>`_ or `develop (unstable !) <https://gitlab.com/slumber/multi-user/-/jobs/artifacts/develop/download?job=build>`_ build.
|
1. Download latest `release <https://gitlab.com/slumber/multi-user/-/jobs/artifacts/master/download?job=build>`_ or `develop (unstable !) <https://gitlab.com/slumber/multi-user/-/jobs/artifacts/develop/download?job=build>`_ build.
|
||||||
2. Run blender as administrator (to allow python dependencies auto-installation).
|
2. Run blender as administrator (to allow python dependencies auto-installation).
|
||||||
3. Install **multi-user.zip** from your addon preferences.
|
3. Install **multi-user.zip** from your addon preferences.
|
||||||
|
|
||||||
|
Once the addon is succesfully installed, I strongly recommend you to follow the :ref:`quickstart`
|
||||||
|
tutorial.
|
@ -1,105 +1,287 @@
|
|||||||
|
.. _quickstart:
|
||||||
|
|
||||||
===========
|
===========
|
||||||
Quick start
|
Quick start
|
||||||
===========
|
===========
|
||||||
|
|
||||||
*All settings are located under: `View3D -> Sidebar -> Multiuser panel`*
|
.. hint::
|
||||||
|
*All session related settings are located under: `View3D -> Sidebar -> Multiuser panel`*
|
||||||
|
|
||||||
Session setup
|
The multi-user is based on a session management system.
|
||||||
=============
|
In this this guide you will quickly learn how to use the collaborative session system in three part:
|
||||||
This section describe how to create or join a collaborative session.
|
|
||||||
|
|
||||||
---------------------
|
- :ref:`how-to-host`
|
||||||
1. User information's
|
- :ref:`how-to-join`
|
||||||
---------------------
|
- :ref:`how-to-manage`
|
||||||
|
|
||||||
.. image:: img/quickstart_user_infos.png
|
.. _how-to-host:
|
||||||
|
|
||||||
- **name**: username.
|
How to host a session
|
||||||
- **color**: color used to represent the user into other user workspace.
|
=====================
|
||||||
|
|
||||||
----------
|
The multi-user add-on rely on a Client-Server architecture.
|
||||||
2. Network
|
The server is the heart of the collaborative session,
|
||||||
----------
|
it will allow each users to communicate with each others.
|
||||||
|
In simple terms, *Hosting a session* means *run a local server and connect the local client to it*.
|
||||||
|
When I said **local server** I mean accessible from the LAN (Local Area Network).
|
||||||
|
|
||||||
.. note:: If you host a session over internet, special network configuration is needed.
|
However sometime you will need to host a session over the internet,
|
||||||
|
in this case I strongly recommand you to read the :ref:`internet-guide` tutorial.
|
||||||
|
|
||||||
Hosting and connection are done from this panel.
|
.. _user-info:
|
||||||
|
|
||||||
+-----------------------------------+-------------------------------------+
|
|
||||||
| 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.
|
1. Fill your user information
|
||||||
If 5555 is given in host settings, it will use 5555, 5556 (5555+1), 5557 (5555+2), 5558 (5555+3).
|
-----------------------------
|
||||||
|
|
||||||
------------
|
The **User Info** panel (See image below) allow you to constomize your online identity.
|
||||||
2.1 Advanced
|
|
||||||
------------
|
|
||||||
|
|
||||||
.. image:: img/quickstart_advanced.png
|
.. figure:: img/quickstart_user_info.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
**Synchronise render settings** (only host) enable replication of EEVEE and CYCLES render settings to match render between clients.
|
User info panel
|
||||||
|
|
||||||
**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:
|
Let's fill those tow field:
|
||||||
|
|
||||||
- **Refresh**: pushed data update rate (in second)
|
- **name**: your online name.
|
||||||
- **Apply**: pulled data update rate (in second)
|
- **color**: a color used to represent you into other user workspace(see image below).
|
||||||
|
|
||||||
.. note:: Per-data type settings will soon be revamped for simplification purposes
|
|
||||||
|
|
||||||
Session Management
|
During online sessions, other users will see your selected object and camera hilghlited in your profile color.
|
||||||
==================
|
|
||||||
|
|
||||||
This section describe tools available during a collaborative session.
|
.. _user-representation:
|
||||||
|
|
||||||
---------------
|
.. figure:: img/quickstart_user_representation.png
|
||||||
Connected users
|
:align: center
|
||||||
|
|
||||||
|
User viewport representation
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
2. Setup the network
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
When the hosting process will start, the multi-user addon will lauch a local server instance.
|
||||||
|
In the nerwork panel select **HOST**.
|
||||||
|
The **Host sub-panel** (see image below) allow you to configure the server according to:
|
||||||
|
|
||||||
|
* **Port**: Port on wich the server is listening.
|
||||||
|
* **Start from**: The session initialisation method.
|
||||||
|
|
||||||
|
* **current scenes**: Start with the current blendfile datas.
|
||||||
|
* **an empty scene**: Clear a data and start over.
|
||||||
|
|
||||||
|
.. danger::
|
||||||
|
By starting from an empty, all of the blend data will be removed !
|
||||||
|
Ensure to save your existing work before launching the session.
|
||||||
|
|
||||||
|
* **Admin password**: The session administration password.
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_host.png
|
||||||
|
:align: center
|
||||||
|
:alt: host menu
|
||||||
|
|
||||||
|
Host network panel
|
||||||
|
|
||||||
|
|
||||||
|
.. note:: Additionnal configuration setting can be found in the :ref:`advanced` section.
|
||||||
|
|
||||||
|
Once everything is setup you can hit the **HOST** button to launch the session !
|
||||||
|
|
||||||
|
It will do two things:
|
||||||
|
|
||||||
|
* Start a local server
|
||||||
|
* Connect you to it as an :ref:`admin`
|
||||||
|
|
||||||
|
During online session, various actions are available to you, go to :ref:`how-to-manage` section to
|
||||||
|
learn more about them.
|
||||||
|
|
||||||
|
.. _how-to-join:
|
||||||
|
|
||||||
|
How to join a session
|
||||||
|
=====================
|
||||||
|
|
||||||
|
This section describe how join a launched session.
|
||||||
|
Before starting make sure that you have access to the session ip and port.
|
||||||
|
|
||||||
|
-----------------------------
|
||||||
|
1. Fill your user information
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Follow the user-info_ section for this step.
|
||||||
|
|
||||||
|
----------------
|
||||||
|
2. Network setup
|
||||||
|
----------------
|
||||||
|
|
||||||
|
In the nerwork panel select **JOIN**.
|
||||||
|
The **join sub-panel** (see image below) allow you configure the client to join a
|
||||||
|
collaborative session.
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_join.png
|
||||||
|
:align: center
|
||||||
|
:alt: Connect menu
|
||||||
|
|
||||||
|
Connection panel
|
||||||
|
|
||||||
|
Fill those field with your information:
|
||||||
|
|
||||||
|
- **IP**: the host ip.
|
||||||
|
- **Port**: the host port.
|
||||||
|
- **Connect as admin**: connect you with **admin rights** (see :ref:`admin` ) to the session.
|
||||||
|
|
||||||
|
.. Maybe something more explicit here
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Additionnal configuration setting can be found in the :ref:`advanced` section.
|
||||||
|
|
||||||
|
Once you've set every field, hit the button **CONNECT** to join the session !
|
||||||
|
When the :ref:`session-status` is **ONLINE** you are online and ready to start to collaborate.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
On the **dedicated server** startup, the session status will get you to the **LOBBY** waiting a admin to start it.
|
||||||
|
|
||||||
|
If the session status is set to **LOBBY** and you are a regular user, you need to wait that an admin launch it.
|
||||||
|
If you are the admin, you just need to init the repository to start the session (see image below).
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_session_init.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Session initialisation for dedicated server
|
||||||
|
|
||||||
|
During online session, various actions are available to you, go to :ref:`how-to-manage` section to
|
||||||
|
learn more about them.
|
||||||
|
|
||||||
|
.. _how-to-manage:
|
||||||
|
|
||||||
|
How to manage a session
|
||||||
|
=======================
|
||||||
|
|
||||||
|
The collaboration quality directly depend on the communication quality. This section describes
|
||||||
|
various tools made in an effort to ease the communication between the different session users.
|
||||||
|
Feel free to suggest any idea for communication tools `here <https://gitlab.com/slumber/multi-user/-/issues/75>`_ .
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
Monitor online users
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
One of the most vital tool is the **Online user panel**. It list all connected
|
||||||
|
users information's including yours such as :
|
||||||
|
|
||||||
|
* **Role** : if user is an admin or a regular user.
|
||||||
|
* **Location**: Where the user is actually working.
|
||||||
|
* **Frame**: When (in frame) the user working.
|
||||||
|
* **Ping**: user connection delay in milliseconds
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_users.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Online user panel
|
||||||
|
|
||||||
|
By selecting a user in the list you'll have access to different user related **actions**.
|
||||||
|
Those operators allow you reach the selected user state in tow different dimensions: **SPACE** and **TIME**.
|
||||||
|
|
||||||
|
Snapping in space
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The **CAMERA button** (Also called **snap view** operator) allow you to snap on
|
||||||
|
the user viewpoint. To disable the snap, click back on the button. This action
|
||||||
|
served different purposes such as easing the review process, working together on
|
||||||
|
wide world.
|
||||||
|
|
||||||
|
.. hint::
|
||||||
|
If the target user is localized on another scene, the **snap view** operator will send you to his scene.
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_snap_view.gif
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Snap view in action
|
||||||
|
|
||||||
|
Snapping in time
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
.. image:: img/quickstart_users.png
|
The **CLOCK button** (Also called **snap time** operator) allow you to snap on
|
||||||
|
the user time (current frame). To disable the snap, click back on the button.
|
||||||
|
This action is built to help various actors to work on the same temporality
|
||||||
|
(for instance multiple animators).
|
||||||
|
|
||||||
This panel displays all connected users information's, including yours.
|
.. figure:: img/quickstart_snap_time.gif
|
||||||
By selecting a user in the list you'll have access to different **actions**:
|
:align: center
|
||||||
|
|
||||||
- The **camera button** allow you to snap on the user viewpoint.
|
Snap time in action
|
||||||
- The **time button** allow you to snap on the user time.
|
|
||||||
- The **cross button** [**host only**] allow the admin to kick users
|
|
||||||
|
|
||||||
-------------------
|
|
||||||
Presence show flags
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
.. image:: img/quickstart_presence.png
|
Kick a user
|
||||||
|
-----------
|
||||||
|
|
||||||
This pannel allow you to tweak users overlay in the viewport:
|
.. warning:: Only available for :ref:`admin` !
|
||||||
|
|
||||||
|
|
||||||
|
The **CROSS button** (Also called **kick** operator) allow the admin to kick the selected user. On the target user side, the session will properly disconnect.
|
||||||
|
|
||||||
|
|
||||||
|
Change users display
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Presence is the multi-user module responsible for users display. During the session,
|
||||||
|
it draw users related information in your viewport such as:
|
||||||
|
|
||||||
|
* Username
|
||||||
|
* User point of view
|
||||||
|
* User selection
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_presence.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Presence show flags
|
||||||
|
|
||||||
|
The presence overlay panel (see image above) allow you to enable/disable
|
||||||
|
various drawn parts via the following flags:
|
||||||
|
|
||||||
- **Show selected objects**: display other users current selection
|
- **Show selected objects**: display other users current selection
|
||||||
- **Show users**: display users current viewpoint
|
- **Show users**: display users current viewpoint
|
||||||
- **Show different scenes**: display users on other scenes
|
- **Show different scenes**: display users working on other scenes
|
||||||
|
|
||||||
---------------------
|
-----------
|
||||||
Replicated properties
|
Manage data
|
||||||
---------------------
|
-----------
|
||||||
|
|
||||||
.. image:: img/quickstart_properties.png
|
In order to understand replication data managment, a quick introduction to the multi-user data workflow is required.
|
||||||
|
First thing to know: until now, the addon rely on a data-based replication. In simple words, it means that it replicate
|
||||||
|
user's action results.
|
||||||
|
To replicate datablocks between clients the multi-user rely on what tends to be a distributed architecture:
|
||||||
|
|
||||||
The **replicated properties** panel shows all replicated properties status and associated actions.
|
- The server store the "master" version of the work.
|
||||||
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.
|
- Each client have a local version of the work.
|
||||||
|
|
||||||
|
When an artist modified something in the scene, here is what is happening in the background:
|
||||||
|
|
||||||
|
1. Modified data are **COMMITTED** to the local repository.
|
||||||
|
2. Once committed locally, they are **PUSHED** to the server
|
||||||
|
3. As soon as the server is getting updates, they are stored locally and pushed to every other clients
|
||||||
|
|
||||||
|
At the top of this data management system, a right management system prevent
|
||||||
|
multiple users from modifying same data at same time. A datablock may belong to
|
||||||
|
a connected user or be under :ref:`common-right<**COMMON**>` rights.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In a near future, the right management system will support roles to allow multiple users to
|
||||||
|
work on different aspect of the same datablock.
|
||||||
|
|
||||||
|
The Repository panel (see image below) allow you to monitor, change datablock states and right manually.
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_properties.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Repository panel
|
||||||
|
|
||||||
|
The **show only owned** flag allow you to see which datablocks you are currently modifying.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
If you are editing a datablock not listed with this fag enabled, it means that you do
|
||||||
|
not have right granted to modify it. So it won't be updated to other client !
|
||||||
|
|
||||||
|
Here is a quick list of available actions:
|
||||||
|
|
||||||
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
|
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
|
||||||
| icon | Action | Description |
|
| icon | Action | Description |
|
||||||
@ -115,12 +297,39 @@ Since the replication architecture is based on commit/push/pull mechanisms, a re
|
|||||||
| .. image:: img/quickstart_remove.png | **Delete** | Remove the data-block from network replication |
|
| .. image:: img/quickstart_remove.png | **Delete** | Remove the data-block from network replication |
|
||||||
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
|
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
.. _advanced:
|
||||||
|
|
||||||
|
Advanced configuration
|
||||||
|
======================
|
||||||
|
|
||||||
|
This section contains optional settings to configure the session behavior.
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_advanced.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Repository panel
|
||||||
|
|
||||||
|
.. rubric:: Network
|
||||||
|
|
||||||
|
**IPC Port** is the port used for Inter Process Communication. This port is used
|
||||||
|
by the multi-users subprocesses to communicate with each others. If different instances
|
||||||
|
of the multi-user are using the same IPC port it will create conflict !
|
||||||
|
|
||||||
|
You only need to modify it if you need to launch multiple clients from the same
|
||||||
|
computer(or if you try to host and join on the same computer). You should just enter a different
|
||||||
|
**IPC port** for each blender instance.
|
||||||
|
|
||||||
|
**Timeout (in milliseconds)** is the maximum ping authorized before auto-disconnecting.
|
||||||
|
You should only increase it if you have a bad connection.
|
||||||
|
|
||||||
|
.. rubric:: Replication
|
||||||
|
|
||||||
|
**Synchronize render settings** (only host) enable replication of EEVEE and CYCLES render settings to match render between clients.
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
@ -18,6 +18,11 @@ Main Features
|
|||||||
- Datablocks right managment
|
- Datablocks right managment
|
||||||
- Tested under Windows
|
- Tested under Windows
|
||||||
|
|
||||||
|
Community
|
||||||
|
=========
|
||||||
|
|
||||||
|
A `discord server <https://discord.gg/aBPvGws>`_ have been created to provide help for new users and
|
||||||
|
organize collaborative creation sessions.
|
||||||
|
|
||||||
Status
|
Status
|
||||||
======
|
======
|
||||||
@ -43,6 +48,7 @@ Documentation is organized into the following sections:
|
|||||||
|
|
||||||
getting_started/install
|
getting_started/install
|
||||||
getting_started/quickstart
|
getting_started/quickstart
|
||||||
|
getting_started/glossary
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
@ -1,47 +1,278 @@
|
|||||||
================
|
.. _internet-guide:
|
||||||
Advanced hosting
|
|
||||||
================
|
|
||||||
|
|
||||||
This tutorial aims to guide you to host a collaborative Session on internet.
|
===================
|
||||||
|
Hosting 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::
|
.. 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>`_).
|
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>`_).
|
||||||
|
|
||||||
|
This tutorial aims to guide you to host a collaborative Session on internet.
|
||||||
|
Hosting a session can be done is several ways:
|
||||||
|
|
||||||
|
- :ref:`host-blender`: hosting a session directly from the blender add-on panel.
|
||||||
|
- :ref:`host-dedicated`: hosting a session directly from the command line interface on a computer without blender.
|
||||||
|
|
||||||
|
.. _host-blender:
|
||||||
|
|
||||||
|
-------------
|
||||||
|
From blender
|
||||||
|
-------------
|
||||||
|
By default your router doesn't allow anyone to share you connection.
|
||||||
|
In order grant server access to people from internet you have tow main option:
|
||||||
|
|
||||||
|
* The :ref:`connection-sharing`: the easiest way.
|
||||||
|
* The :ref:`port-forwarding`: this one is the most unsecure, if you have no networking knowledge, you should definitively go to :ref:`connection-sharing`.
|
||||||
|
|
||||||
|
.. _connection-sharing:
|
||||||
|
|
||||||
|
Using a connection sharing solution
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
Many third party software like `ZEROTIER <https://www.zerotier.com/download/>`_ (Free) or `HAMACHI <https://vpn.net/>`_ (Free until 5 users) allow you to share your private network with other people.
|
||||||
|
For the example I'm gonna use ZeroTier because its free and open source.
|
||||||
|
|
||||||
|
1. Installation
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Let's start by downloading and installing ZeroTier:
|
||||||
|
https://www.zerotier.com/download/
|
||||||
|
|
||||||
|
Once installed, launch it.
|
||||||
|
|
||||||
|
2. Network creation
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
To create a ZeroTier private network you need to register a ZeroTier account `on my.zerotier.com <https://my.zerotier.com/login>`_
|
||||||
|
(click on **login** then register on the bottom)
|
||||||
|
|
||||||
|
Once you account it activated, you can connect to `my.zerotier.com <https://my.zerotier.com/login>`_.
|
||||||
|
Head up to the **Network** section(highlighted in red in the image below).
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_head_network.png
|
||||||
|
:align: center
|
||||||
|
:width: 450px
|
||||||
|
|
||||||
|
ZeroTier user homepage
|
||||||
|
|
||||||
|
Hit 'Create a network'(see image below) and go to the network settings.
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_create_network.png
|
||||||
|
:align: center
|
||||||
|
:width: 450px
|
||||||
|
|
||||||
|
Network page
|
||||||
|
|
||||||
|
Now that the network is created, let's configure it.
|
||||||
|
|
||||||
|
In the Settings section(see image below), you can change the network name to what you want.
|
||||||
|
Make sure that the field **Access Control** is set to **PRIVATE**.
|
||||||
|
|
||||||
|
.. hint::
|
||||||
|
If you set the Access Control to PUBLIC, anyone will be able to join without
|
||||||
|
your confirmation. It is easier to set up but less secure.
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_network_settings.png
|
||||||
|
:align: center
|
||||||
|
:width: 450px
|
||||||
|
|
||||||
|
Network settings
|
||||||
|
|
||||||
|
That's all for the network setup !
|
||||||
|
Now let's connect everyone.
|
||||||
|
|
||||||
|
.. _network-authorization:
|
||||||
|
|
||||||
|
3. Network authorization
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Since your ZeroTier network is Private, you will need to authorize each new users
|
||||||
|
to connect to it.
|
||||||
|
For each user you want to add, do the following step:
|
||||||
|
|
||||||
|
1. Get the client **ZeroTier id** by right clicking on the ZeroTier tray icon and click on the `Node ID`, it will copy it.
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_get_node.png
|
||||||
|
:align: center
|
||||||
|
:width: 450px
|
||||||
|
|
||||||
|
Get the ZeroTier client id
|
||||||
|
|
||||||
|
2. Go to the network settings in the Member section and paste the Node ID into the Manually Add Member field.
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_add_node.png
|
||||||
|
:align: center
|
||||||
|
:width: 450px
|
||||||
|
|
||||||
|
Add the client to network authorized users
|
||||||
|
|
||||||
|
4. Network connection
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
To connect to the ZeroTier network, get the network id from the network settings (see image).
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_get_id.png
|
||||||
|
:align: center
|
||||||
|
:width: 450px
|
||||||
|
|
||||||
|
Now we are ready to join the network !
|
||||||
|
Right click on the ZeroTier tray icon and select **Join Network** !
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_join_network.png
|
||||||
|
:align: center
|
||||||
|
:width: 450px
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_join.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Joining the network
|
||||||
|
|
||||||
|
Past the network id and check ``Allow Managed`` then click on join !
|
||||||
|
You should be connected to the network.
|
||||||
|
|
||||||
|
Let's check the connection status. Right click on the tray icon and click on **Show Networks...**.
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_show_network.png
|
||||||
|
:align: center
|
||||||
|
:width: 450px
|
||||||
|
|
||||||
|
Show network status
|
||||||
|
|
||||||
|
.. figure:: img/hosting_guide_network_status.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Network status.
|
||||||
|
|
||||||
|
The network status must be **OK** for each user(like in the picture above) otherwise it means that you are not connected to the network.
|
||||||
|
If you see something like **ACCESS_DENIED**, it means that you were not authorized to join the network. Please check the :ref:`network-authorization` section.
|
||||||
|
|
||||||
|
This is it for the ZeroTier network setup. Now everything should be setup to use the multi-user add-on over internet ! You can now follow the :ref:`quickstart` guide to start using the multi-user add-on !
|
||||||
|
|
||||||
|
.. _port-forwarding:
|
||||||
|
|
||||||
|
Using port-forwarding
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The port forwarding method consist to configure you Network route to allow internet trafic throught specific ports.
|
||||||
|
|
||||||
|
In order to know which port are used by the add-on, check the :ref:`port-setup` section.
|
||||||
|
To set up port forwarding for each port you can follow this `guide <https://www.wikihow.com/Set-Up-Port-Forwarding-on-a-Router>`_ for example.
|
||||||
|
|
||||||
|
Once you have set up the network you can follow the :ref:`quickstart` guide to start using the multi-user add-on !
|
||||||
|
|
||||||
|
.. _host-dedicated:
|
||||||
|
|
||||||
|
--------------------------
|
||||||
|
From the dedicated server
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
The dedicated server is developed to run directly on internet server (like VPS). You can also
|
||||||
|
run it at home for LAN but for internet hosting you need to follow the :ref:`port-forwarding` setup first.
|
||||||
|
|
||||||
|
The dedicated server allow you to host a session with simplicity from any location.
|
||||||
|
It was developed to improve intaernet hosting performance.
|
||||||
|
|
||||||
|
The dedicated server can be run in tow ways:
|
||||||
|
|
||||||
|
- :ref:`cmd-line`
|
||||||
|
- :ref:`docker`
|
||||||
|
|
||||||
|
.. _cmd-line:
|
||||||
|
|
||||||
|
Using a regular command line
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
You can run the dedicated server on any platform by following those steps:
|
||||||
|
|
||||||
|
1. Firstly, download and intall python 3 (3.6 or above).
|
||||||
|
2. Install the replication library:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python -m pip install replication
|
||||||
|
|
||||||
|
4. Launch the server with:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
replication.serve
|
||||||
|
|
||||||
|
.. hint::
|
||||||
|
You can also specify a custom **port** (-p), **timeout** (-t) and **admin password** (-pwd) with the following optionnal argument
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
replication.serve -p 5555 -pwd toto -t 1000
|
||||||
|
|
||||||
|
As soon as the dedicated server is running, you can connect to it from blender (follow :ref:`how-to-join`).
|
||||||
|
|
||||||
|
|
||||||
|
.. hint::
|
||||||
|
Some commands are available to manage the session. Check :ref:`dedicated-management` to learn more.
|
||||||
|
|
||||||
|
|
||||||
|
.. _docker:
|
||||||
|
|
||||||
|
Using a pre-configured image on docker engine
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
Launching the dedicated server from a docker server is simple as:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
-p 5555-5560:5555-5560 \
|
||||||
|
-e port=5555 \
|
||||||
|
-e password=admin \
|
||||||
|
-e timeout=1000 \
|
||||||
|
registry.gitlab.com/slumber/multi-user/multi-user-server:0.0.3
|
||||||
|
|
||||||
|
As soon as the dedicated server is running, you can connect to it from blender.
|
||||||
|
You can check the :ref:`how-to-join` section.
|
||||||
|
|
||||||
|
.. hint::
|
||||||
|
Some commands are available to manage the session. Check :ref:`dedicated-management` to learn more.
|
||||||
|
|
||||||
|
.. _dedicated-management:
|
||||||
|
|
||||||
|
Dedicated server management
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Here is the list of available commands from the dedicated server:
|
||||||
|
|
||||||
|
- ``help``: Show all commands.
|
||||||
|
- ``exit`` or ``Ctrl+C`` : Stop the server.
|
||||||
|
- ``kick username``: kick the provided user.
|
||||||
|
- ``users``: list all online users.
|
||||||
|
|
||||||
|
|
||||||
|
.. _port-setup:
|
||||||
|
|
||||||
|
----------
|
||||||
|
Port setup
|
||||||
|
----------
|
||||||
|
|
||||||
|
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.) [given port]
|
||||||
|
* Subscriber : pull data [Commands port + 1]
|
||||||
|
* Publisher : push data [Commands port + 2]
|
||||||
|
* TTL (time to leave) : used to ping each client [Commands port + 3]
|
||||||
|
|
||||||
To know which ports will be used, you just have to read the port in your preference.
|
To know which ports will be used, you just have to read the port in your preference.
|
||||||
|
|
||||||
.. image:: img/hosting_guide_port.png
|
.. figure:: img/hosting_guide_port.png
|
||||||
:align: center
|
:align: center
|
||||||
:alt: Port
|
:alt: Port
|
||||||
|
:width: 200px
|
||||||
|
|
||||||
|
Port in host settings
|
||||||
In the picture below we have setup our port to **5555** so it will be:
|
In the picture below we have setup our port to **5555** so it will be:
|
||||||
|
|
||||||
* Commands: 5555 (**5555** +0)
|
* Commands: 5555 (**5555**)
|
||||||
* Subscriber: 5556 (**5555** +1)
|
* Subscriber: 5556 (**5555** +1)
|
||||||
* Publisher: 5557 (**5555** +2)
|
* Publisher: 5557 (**5555** +2)
|
||||||
* TTL: 5558 (**5555** +3)
|
* 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.
|
Those four ports need to be accessible from the client otherwise it won't work at all !
|
||||||
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_add_node.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
docs/tutorials/img/hosting_guide_create_network.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
docs/tutorials/img/hosting_guide_get_id.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
docs/tutorials/img/hosting_guide_get_node.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
docs/tutorials/img/hosting_guide_head_network.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/tutorials/img/hosting_guide_join.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
docs/tutorials/img/hosting_guide_join_network.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/tutorials/img/hosting_guide_network_settings.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
docs/tutorials/img/hosting_guide_network_status.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 20 KiB |
BIN
docs/tutorials/img/hosting_guide_show_network.png
Normal file
After Width: | Height: | Size: 19 KiB |
@ -21,7 +21,7 @@ bl_info = {
|
|||||||
"author": "Swann Martinez",
|
"author": "Swann Martinez",
|
||||||
"version": (0, 0, 3),
|
"version": (0, 0, 3),
|
||||||
"description": "Enable real-time collaborative workflow inside blender",
|
"description": "Enable real-time collaborative workflow inside blender",
|
||||||
"blender": (2, 80, 0),
|
"blender": (2, 82, 0),
|
||||||
"location": "3D View > Sidebar > Multi-User tab",
|
"location": "3D View > Sidebar > Multi-User tab",
|
||||||
"warning": "Unstable addon, use it at your own risks",
|
"warning": "Unstable addon, use it at your own risks",
|
||||||
"category": "Collaboration",
|
"category": "Collaboration",
|
||||||
@ -45,21 +45,15 @@ from . import environment, utils
|
|||||||
|
|
||||||
# TODO: remove dependency as soon as replication will be installed as a module
|
# TODO: remove dependency as soon as replication will be installed as a module
|
||||||
DEPENDENCIES = {
|
DEPENDENCIES = {
|
||||||
("zmq","zmq"),
|
("replication", '0.0.20'),
|
||||||
("jsondiff","jsondiff"),
|
("deepdiff", '5.0.1'),
|
||||||
("deepdiff", "deepdiff")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
libs = os.path.dirname(os.path.abspath(__file__))+"\\libs\\replication\\replication"
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
# Setup logging policy
|
# Setup logging policy
|
||||||
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
|
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
|
||||||
|
|
||||||
if libs not in sys.path:
|
|
||||||
sys.path.append(libs)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
environment.setup(DEPENDENCIES, bpy.app.binary_path_python)
|
environment.setup(DEPENDENCIES, bpy.app.binary_path_python)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
@ -80,7 +74,9 @@ def register():
|
|||||||
|
|
||||||
bpy.types.WindowManager.session = bpy.props.PointerProperty(
|
bpy.types.WindowManager.session = bpy.props.PointerProperty(
|
||||||
type=preferences.SessionProps)
|
type=preferences.SessionProps)
|
||||||
bpy.types.ID.uuid = bpy.props.StringProperty(default="")
|
bpy.types.ID.uuid = bpy.props.StringProperty(
|
||||||
|
default="",
|
||||||
|
options={'HIDDEN', 'SKIP_SAVE'})
|
||||||
bpy.types.WindowManager.online_users = bpy.props.CollectionProperty(
|
bpy.types.WindowManager.online_users = bpy.props.CollectionProperty(
|
||||||
type=preferences.SessionUser
|
type=preferences.SessionUser
|
||||||
)
|
)
|
||||||
|
@ -722,19 +722,20 @@ class Singleton_updater(object):
|
|||||||
|
|
||||||
self._source_zip = os.path.join(local,"source.zip")
|
self._source_zip = os.path.join(local,"source.zip")
|
||||||
|
|
||||||
if self._verbose: print("Starting download update zip")
|
if self._verbose: print(f"Starting download update zip to {self._source_zip}")
|
||||||
try:
|
try:
|
||||||
request = urllib.request.Request(url)
|
import urllib3
|
||||||
context = ssl._create_unverified_context()
|
http = urllib3.PoolManager()
|
||||||
|
r = http.request('GET', url, preload_content=False)
|
||||||
|
chunk_size = 1024*8
|
||||||
|
with open(self._source_zip, 'wb') as out:
|
||||||
|
while True:
|
||||||
|
data = r.read(chunk_size)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
out.write(data)
|
||||||
|
|
||||||
# setup private token if appropriate
|
r.release_conn()
|
||||||
if self._engine.token != None:
|
|
||||||
if self._engine.name == "gitlab":
|
|
||||||
request.add_header('PRIVATE-TOKEN',self._engine.token)
|
|
||||||
else:
|
|
||||||
if self._verbose: print("Tokens not setup for selected engine yet")
|
|
||||||
self.urlretrieve(urllib.request.urlopen(request,context=context), self._source_zip)
|
|
||||||
# add additional checks on file size being non-zero
|
|
||||||
if self._verbose: print("Successfully downloaded update zip")
|
if self._verbose: print("Successfully downloaded update zip")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -38,7 +38,7 @@ __all__ = [
|
|||||||
] # Order here defines execution order
|
] # Order here defines execution order
|
||||||
|
|
||||||
from . import *
|
from . import *
|
||||||
from ..libs.replication.replication.data import ReplicatedDataFactory
|
from replication.data import ReplicatedDataFactory
|
||||||
|
|
||||||
def types_to_register():
|
def types_to_register():
|
||||||
return __all__
|
return __all__
|
||||||
|
@ -92,6 +92,7 @@ class BlArmature(BlDatablock):
|
|||||||
new_bone.head = bone_data['head_local']
|
new_bone.head = bone_data['head_local']
|
||||||
new_bone.tail_radius = bone_data['tail_radius']
|
new_bone.tail_radius = bone_data['tail_radius']
|
||||||
new_bone.head_radius = bone_data['head_radius']
|
new_bone.head_radius = bone_data['head_radius']
|
||||||
|
# new_bone.roll = bone_data['roll']
|
||||||
|
|
||||||
if 'parent' in bone_data:
|
if 'parent' in bone_data:
|
||||||
new_bone.parent = target.edit_bones[data['bones']
|
new_bone.parent = target.edit_bones[data['bones']
|
||||||
@ -123,7 +124,8 @@ class BlArmature(BlDatablock):
|
|||||||
'use_connect',
|
'use_connect',
|
||||||
'parent',
|
'parent',
|
||||||
'name',
|
'name',
|
||||||
'layers'
|
'layers',
|
||||||
|
# 'roll',
|
||||||
|
|
||||||
]
|
]
|
||||||
data = dumper.dump(instance)
|
data = dumper.dump(instance)
|
||||||
|
@ -36,7 +36,7 @@ class BlCamera(BlDatablock):
|
|||||||
|
|
||||||
|
|
||||||
def _load_implementation(self, data, target):
|
def _load_implementation(self, data, target):
|
||||||
loader = Loader()
|
loader = Loader()
|
||||||
loader.load(target, data)
|
loader.load(target, data)
|
||||||
|
|
||||||
dof_settings = data.get('dof')
|
dof_settings = data.get('dof')
|
||||||
@ -45,13 +45,22 @@ class BlCamera(BlDatablock):
|
|||||||
if dof_settings:
|
if dof_settings:
|
||||||
loader.load(target.dof, dof_settings)
|
loader.load(target.dof, dof_settings)
|
||||||
|
|
||||||
|
background_images = data.get('background_images')
|
||||||
|
|
||||||
|
if background_images:
|
||||||
|
target.background_images.clear()
|
||||||
|
for img_name, img_data in background_images.items():
|
||||||
|
target_img = target.background_images.new()
|
||||||
|
target_img.image = bpy.data.images[img_name]
|
||||||
|
loader.load(target_img, img_data)
|
||||||
|
|
||||||
def _dump_implementation(self, data, instance=None):
|
def _dump_implementation(self, data, instance=None):
|
||||||
assert(instance)
|
assert(instance)
|
||||||
|
|
||||||
# TODO: background image support
|
# TODO: background image support
|
||||||
|
|
||||||
dumper = Dumper()
|
dumper = Dumper()
|
||||||
dumper.depth = 2
|
dumper.depth = 3
|
||||||
dumper.include_filter = [
|
dumper.include_filter = [
|
||||||
"name",
|
"name",
|
||||||
'type',
|
'type',
|
||||||
@ -79,7 +88,24 @@ class BlCamera(BlDatablock):
|
|||||||
'sensor_fit',
|
'sensor_fit',
|
||||||
'sensor_height',
|
'sensor_height',
|
||||||
'sensor_width',
|
'sensor_width',
|
||||||
|
'show_background_images',
|
||||||
|
'background_images',
|
||||||
|
'alpha',
|
||||||
|
'display_depth',
|
||||||
|
'frame_method',
|
||||||
|
'offset',
|
||||||
|
'rotation',
|
||||||
|
'scale',
|
||||||
|
'use_flip_x',
|
||||||
|
'use_flip_y',
|
||||||
|
'image'
|
||||||
]
|
]
|
||||||
return dumper.dump(instance)
|
return dumper.dump(instance)
|
||||||
|
|
||||||
|
def _resolve_deps_implementation(self):
|
||||||
|
deps = []
|
||||||
|
for background in self.instance.background_images:
|
||||||
|
if background.image:
|
||||||
|
deps.append(background.image)
|
||||||
|
|
||||||
|
return deps
|
||||||
|
@ -21,8 +21,8 @@ import mathutils
|
|||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
from .dump_anything import Loader, Dumper
|
from .dump_anything import Loader, Dumper
|
||||||
from ..libs.replication.replication.data import ReplicatedDatablock
|
from replication.data import ReplicatedDatablock
|
||||||
from ..libs.replication.replication.constants import (UP, DIFF_BINARY)
|
from replication.constants import (UP, DIFF_BINARY)
|
||||||
|
|
||||||
|
|
||||||
def has_action(target):
|
def has_action(target):
|
||||||
@ -107,24 +107,25 @@ class BlDatablock(ReplicatedDatablock):
|
|||||||
(self.data and 'library' in self.data)
|
(self.data and 'library' in self.data)
|
||||||
|
|
||||||
if instance and hasattr(instance, 'uuid'):
|
if instance and hasattr(instance, 'uuid'):
|
||||||
instance.uuid = self.uuid
|
instance.uuid = self.uuid
|
||||||
|
|
||||||
# self.diff_method = DIFF_BINARY
|
|
||||||
|
|
||||||
|
self.diff_method = DIFF_BINARY
|
||||||
|
|
||||||
def _resolve(self):
|
def resolve(self):
|
||||||
datablock_ref = None
|
datablock_ref = None
|
||||||
datablock_root = getattr(bpy.data, self.bl_id)
|
datablock_root = getattr(bpy.data, self.bl_id)
|
||||||
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
|
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
|
||||||
|
|
||||||
# In case of lost uuid (ex: undo), resolve by name and reassign it
|
|
||||||
if not datablock_ref:
|
if not datablock_ref:
|
||||||
datablock_ref = datablock_root.get(self.data['name'])
|
try:
|
||||||
|
datablock_ref = datablock_root[self.data['name']]
|
||||||
|
except Exception:
|
||||||
|
datablock_ref = self._construct(data=self.data)
|
||||||
|
|
||||||
if datablock_ref:
|
if datablock_ref:
|
||||||
setattr(datablock_ref, 'uuid', self.uuid)
|
setattr(datablock_ref, 'uuid', self.uuid)
|
||||||
|
|
||||||
return datablock_ref
|
self.instance = datablock_ref
|
||||||
|
|
||||||
def _dump(self, instance=None):
|
def _dump(self, instance=None):
|
||||||
dumper = Dumper()
|
dumper = Dumper()
|
||||||
@ -142,7 +143,7 @@ class BlDatablock(ReplicatedDatablock):
|
|||||||
dump_driver(driver))
|
dump_driver(driver))
|
||||||
|
|
||||||
data.update(dumped_drivers)
|
data.update(dumped_drivers)
|
||||||
|
|
||||||
if self.is_library:
|
if self.is_library:
|
||||||
data.update(dumper.dump(instance))
|
data.update(dumper.dump(instance))
|
||||||
else:
|
else:
|
||||||
|
@ -35,7 +35,6 @@ def dump_image(image):
|
|||||||
os.makedirs(prefs.cache_directory, exist_ok=True)
|
os.makedirs(prefs.cache_directory, exist_ok=True)
|
||||||
image.file_format = "PNG"
|
image.file_format = "PNG"
|
||||||
image.save()
|
image.save()
|
||||||
logging.info( image.filepath_raw )
|
|
||||||
|
|
||||||
if image.source == "FILE":
|
if image.source == "FILE":
|
||||||
image_path = bpy.path.abspath(image.filepath_raw)
|
image_path = bpy.path.abspath(image.filepath_raw)
|
||||||
|
@ -21,7 +21,7 @@ import mathutils
|
|||||||
|
|
||||||
from .dump_anything import Dumper, Loader, np_dump_collection, np_load_collection
|
from .dump_anything import Dumper, Loader, np_dump_collection, np_load_collection
|
||||||
from .bl_datablock import BlDatablock
|
from .bl_datablock import BlDatablock
|
||||||
from ..libs.replication.replication.exception import ContextError
|
from replication.exception import ContextError
|
||||||
|
|
||||||
POINT = ['co', 'weight_softbody', 'co_deform']
|
POINT = ['co', 'weight_softbody', 'co_deform']
|
||||||
|
|
||||||
|
@ -19,11 +19,13 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import mathutils
|
import mathutils
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
from .dump_anything import Loader, Dumper
|
from .dump_anything import Loader, Dumper
|
||||||
from .bl_datablock import BlDatablock
|
from .bl_datablock import BlDatablock
|
||||||
|
|
||||||
|
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
||||||
|
|
||||||
def load_node(node_data, node_tree):
|
def load_node(node_data, node_tree):
|
||||||
""" Load a node into a node_tree from a dict
|
""" Load a node into a node_tree from a dict
|
||||||
@ -36,21 +38,20 @@ def load_node(node_data, node_tree):
|
|||||||
loader = Loader()
|
loader = Loader()
|
||||||
target_node = node_tree.nodes.new(type=node_data["bl_idname"])
|
target_node = node_tree.nodes.new(type=node_data["bl_idname"])
|
||||||
|
|
||||||
loader.load(target_node, node_data)
|
loader.load(target_node, node_data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for input in node_data["inputs"]:
|
for input in node_data["inputs"]:
|
||||||
if hasattr(target_node.inputs[input], "default_value"):
|
if hasattr(target_node.inputs[input], "default_value"):
|
||||||
try:
|
try:
|
||||||
target_node.inputs[input].default_value = node_data["inputs"][input]["default_value"]
|
target_node.inputs[input].default_value = node_data["inputs"][input]["default_value"]
|
||||||
except:
|
except:
|
||||||
logging.error(f"Material {input} parameter not supported, skipping")
|
logging.error(
|
||||||
|
f"Material {input} parameter not supported, skipping")
|
||||||
|
|
||||||
|
|
||||||
def load_links(links_data, node_tree):
|
def load_links(links_data, node_tree):
|
||||||
""" Load node_tree links from a list
|
""" Load node_tree links from a list
|
||||||
|
|
||||||
:arg links_data: dumped node links
|
:arg links_data: dumped node links
|
||||||
:type links_data: list
|
:type links_data: list
|
||||||
:arg node_tree: node links collection
|
:arg node_tree: node links collection
|
||||||
@ -60,7 +61,6 @@ def load_links(links_data, node_tree):
|
|||||||
for link in links_data:
|
for link in links_data:
|
||||||
input_socket = node_tree.nodes[link['to_node']].inputs[int(link['to_socket'])]
|
input_socket = node_tree.nodes[link['to_node']].inputs[int(link['to_socket'])]
|
||||||
output_socket = node_tree.nodes[link['from_node']].outputs[int(link['from_socket'])]
|
output_socket = node_tree.nodes[link['from_node']].outputs[int(link['from_socket'])]
|
||||||
|
|
||||||
node_tree.links.new(input_socket, output_socket)
|
node_tree.links.new(input_socket, output_socket)
|
||||||
|
|
||||||
|
|
||||||
@ -75,11 +75,13 @@ def dump_links(links):
|
|||||||
links_data = []
|
links_data = []
|
||||||
|
|
||||||
for link in links:
|
for link in links:
|
||||||
|
to_socket = NODE_SOCKET_INDEX.search(link.to_socket.path_from_id()).group(1)
|
||||||
|
from_socket = NODE_SOCKET_INDEX.search(link.from_socket.path_from_id()).group(1)
|
||||||
links_data.append({
|
links_data.append({
|
||||||
'to_node':link.to_node.name,
|
'to_node': link.to_node.name,
|
||||||
'to_socket':link.to_socket.path_from_id()[-2:-1],
|
'to_socket': to_socket,
|
||||||
'from_node':link.from_node.name,
|
'from_node': link.from_node.name,
|
||||||
'from_socket':link.from_socket.path_from_id()[-2:-1],
|
'from_socket': from_socket,
|
||||||
})
|
})
|
||||||
|
|
||||||
return links_data
|
return links_data
|
||||||
@ -118,7 +120,7 @@ def dump_node(node):
|
|||||||
"outputs",
|
"outputs",
|
||||||
"width_hidden"
|
"width_hidden"
|
||||||
]
|
]
|
||||||
|
|
||||||
dumped_node = node_dumper.dump(node)
|
dumped_node = node_dumper.dump(node)
|
||||||
|
|
||||||
if hasattr(node, 'inputs'):
|
if hasattr(node, 'inputs'):
|
||||||
@ -151,7 +153,7 @@ def dump_node(node):
|
|||||||
'location'
|
'location'
|
||||||
]
|
]
|
||||||
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
|
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
|
||||||
|
|
||||||
return dumped_node
|
return dumped_node
|
||||||
|
|
||||||
|
|
||||||
@ -176,15 +178,14 @@ class BlMaterial(BlDatablock):
|
|||||||
loader.load(
|
loader.load(
|
||||||
target.grease_pencil, data['grease_pencil'])
|
target.grease_pencil, data['grease_pencil'])
|
||||||
|
|
||||||
|
|
||||||
if data["use_nodes"]:
|
if data["use_nodes"]:
|
||||||
if target.node_tree is None:
|
if target.node_tree is None:
|
||||||
target.use_nodes = True
|
target.use_nodes = True
|
||||||
|
|
||||||
target.node_tree.nodes.clear()
|
target.node_tree.nodes.clear()
|
||||||
|
|
||||||
loader.load(target,data)
|
loader.load(target, data)
|
||||||
|
|
||||||
# Load nodes
|
# Load nodes
|
||||||
for node in data["node_tree"]["nodes"]:
|
for node in data["node_tree"]["nodes"]:
|
||||||
load_node(data["node_tree"]["nodes"][node], target.node_tree)
|
load_node(data["node_tree"]["nodes"][node], target.node_tree)
|
||||||
@ -221,9 +222,9 @@ class BlMaterial(BlDatablock):
|
|||||||
for node in instance.node_tree.nodes:
|
for node in instance.node_tree.nodes:
|
||||||
nodes[node.name] = dump_node(node)
|
nodes[node.name] = dump_node(node)
|
||||||
data["node_tree"]['nodes'] = nodes
|
data["node_tree"]['nodes'] = nodes
|
||||||
|
|
||||||
data["node_tree"]["links"] = dump_links(instance.node_tree.links)
|
data["node_tree"]["links"] = dump_links(instance.node_tree.links)
|
||||||
|
|
||||||
if instance.is_grease_pencil:
|
if instance.is_grease_pencil:
|
||||||
gp_mat_dumper = Dumper()
|
gp_mat_dumper = Dumper()
|
||||||
gp_mat_dumper.depth = 3
|
gp_mat_dumper.depth = 3
|
||||||
@ -248,7 +249,7 @@ class BlMaterial(BlDatablock):
|
|||||||
'texture_clamp',
|
'texture_clamp',
|
||||||
'gradient_type',
|
'gradient_type',
|
||||||
'mix_color',
|
'mix_color',
|
||||||
'flip'
|
'flip'
|
||||||
]
|
]
|
||||||
data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil)
|
data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil)
|
||||||
return data
|
return data
|
||||||
@ -265,4 +266,3 @@ class BlMaterial(BlDatablock):
|
|||||||
deps.append(self.instance.library)
|
deps.append(self.instance.library)
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ import logging
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection
|
from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection
|
||||||
from ..libs.replication.replication.constants import DIFF_BINARY
|
from replication.constants import DIFF_BINARY
|
||||||
from ..libs.replication.replication.exception import ContextError
|
from replication.exception import ContextError
|
||||||
from .bl_datablock import BlDatablock
|
from .bl_datablock import BlDatablock
|
||||||
|
|
||||||
|
|
||||||
@ -61,7 +61,9 @@ class BlMesh(BlDatablock):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
def _load_implementation(self, data, target):
|
def _load_implementation(self, data, target):
|
||||||
if not target or not target.is_editmode:
|
if not target or target.is_editmode:
|
||||||
|
raise ContextError
|
||||||
|
else:
|
||||||
loader = Loader()
|
loader = Loader()
|
||||||
loader.load(target, data)
|
loader.load(target, data)
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import logging
|
|||||||
|
|
||||||
from .dump_anything import Loader, Dumper
|
from .dump_anything import Loader, Dumper
|
||||||
from .bl_datablock import BlDatablock
|
from .bl_datablock import BlDatablock
|
||||||
from ..libs.replication.replication.exception import ContextError
|
from replication.exception import ContextError
|
||||||
|
|
||||||
|
|
||||||
def load_pose(target_bone, data):
|
def load_pose(target_bone, data):
|
||||||
@ -87,39 +87,8 @@ class BlObject(BlDatablock):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
def _load_implementation(self, data, target):
|
def _load_implementation(self, data, target):
|
||||||
# Load transformation data
|
|
||||||
loader = Loader()
|
loader = Loader()
|
||||||
loader.load(target, data)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
loader.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():
|
|
||||||
loader.load(target_bone, bone_data['constraints'])
|
|
||||||
|
|
||||||
|
|
||||||
load_pose(target_bone, bone_data)
|
|
||||||
|
|
||||||
if 'bone_index' in bone_data.keys():
|
|
||||||
target_bone.bone_group = target.pose.bone_group[bone_data['bone_group_index']]
|
|
||||||
|
|
||||||
# vertex groups
|
# vertex groups
|
||||||
if 'vertex_groups' in data:
|
if 'vertex_groups' in data:
|
||||||
target.vertex_groups.clear()
|
target.vertex_groups.clear()
|
||||||
@ -152,6 +121,45 @@ class BlObject(BlDatablock):
|
|||||||
|
|
||||||
target.data.shape_keys.key_blocks[key_block].relative_key = target.data.shape_keys.key_blocks[reference]
|
target.data.shape_keys.key_blocks[key_block].relative_key = target.data.shape_keys.key_blocks[reference]
|
||||||
|
|
||||||
|
# Load transformation data
|
||||||
|
loader.load(target, data)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
loader.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():
|
||||||
|
loader.load(target_bone, bone_data['constraints'])
|
||||||
|
|
||||||
|
|
||||||
|
load_pose(target_bone, bone_data)
|
||||||
|
|
||||||
|
if 'bone_index' in bone_data.keys():
|
||||||
|
target_bone.bone_group = target.pose.bone_group[bone_data['bone_group_index']]
|
||||||
|
|
||||||
|
# TODO: find another way...
|
||||||
|
if target.type == 'EMPTY':
|
||||||
|
img_key = data.get('data')
|
||||||
|
|
||||||
|
if target.data is None and img_key:
|
||||||
|
target.data = bpy.data.images.get(img_key, None)
|
||||||
|
|
||||||
def _dump_implementation(self, data, instance=None):
|
def _dump_implementation(self, data, instance=None):
|
||||||
assert(instance)
|
assert(instance)
|
||||||
|
|
||||||
@ -171,10 +179,21 @@ class BlObject(BlDatablock):
|
|||||||
"library",
|
"library",
|
||||||
"empty_display_type",
|
"empty_display_type",
|
||||||
"empty_display_size",
|
"empty_display_size",
|
||||||
|
"empty_image_offset",
|
||||||
|
"empty_image_depth",
|
||||||
|
"empty_image_side",
|
||||||
|
"show_empty_image_orthographic",
|
||||||
|
"show_empty_image_perspective",
|
||||||
|
"show_empty_image_only_axis_aligned",
|
||||||
|
"use_empty_image_alpha",
|
||||||
|
"color"
|
||||||
"instance_collection",
|
"instance_collection",
|
||||||
"instance_type",
|
"instance_type",
|
||||||
"location",
|
"location",
|
||||||
"scale",
|
"scale",
|
||||||
|
'lock_location',
|
||||||
|
'lock_rotation',
|
||||||
|
'lock_scale',
|
||||||
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
|
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -186,7 +205,7 @@ class BlObject(BlDatablock):
|
|||||||
# MODIFIERS
|
# MODIFIERS
|
||||||
if hasattr(instance, 'modifiers'):
|
if hasattr(instance, 'modifiers'):
|
||||||
dumper.include_filter = None
|
dumper.include_filter = None
|
||||||
dumper.depth = 2
|
dumper.depth = 1
|
||||||
data["modifiers"] = {}
|
data["modifiers"] = {}
|
||||||
for index, modifier in enumerate(instance.modifiers):
|
for index, modifier in enumerate(instance.modifiers):
|
||||||
data["modifiers"][modifier.name] = dumper.dump(modifier)
|
data["modifiers"][modifier.name] = dumper.dump(modifier)
|
||||||
|
@ -301,7 +301,7 @@ class Dumper:
|
|||||||
self._dump_ID = (lambda x, depth: x.name, self._dump_default_as_branch)
|
self._dump_ID = (lambda x, depth: x.name, self._dump_default_as_branch)
|
||||||
self._dump_collection = (
|
self._dump_collection = (
|
||||||
self._dump_default_as_leaf, self._dump_collection_as_branch)
|
self._dump_default_as_leaf, self._dump_collection_as_branch)
|
||||||
self._dump_array = (self._dump_default_as_leaf,
|
self._dump_array = (self._dump_array_as_branch,
|
||||||
self._dump_array_as_branch)
|
self._dump_array_as_branch)
|
||||||
self._dump_matrix = (self._dump_matrix_as_leaf,
|
self._dump_matrix = (self._dump_matrix_as_leaf,
|
||||||
self._dump_matrix_as_leaf)
|
self._dump_matrix_as_leaf)
|
||||||
|
@ -20,7 +20,14 @@ import logging
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from . import operators, presence, utils
|
from . import operators, presence, utils
|
||||||
from .libs.replication.replication.constants import FETCHED, RP_COMMON, STATE_INITIAL,STATE_QUITTING, STATE_ACTIVE, STATE_SYNCING, STATE_SRV_SYNC
|
from replication.constants import (FETCHED,
|
||||||
|
RP_COMMON,
|
||||||
|
STATE_INITIAL,
|
||||||
|
STATE_QUITTING,
|
||||||
|
STATE_ACTIVE,
|
||||||
|
STATE_SYNCING,
|
||||||
|
STATE_LOBBY,
|
||||||
|
STATE_SRV_SYNC)
|
||||||
|
|
||||||
|
|
||||||
class Delayable():
|
class Delayable():
|
||||||
@ -107,71 +114,66 @@ class DynamicRightSelectTimer(Timer):
|
|||||||
if self._user is None:
|
if self._user is None:
|
||||||
self._user = session.online_users.get(settings.username)
|
self._user = session.online_users.get(settings.username)
|
||||||
|
|
||||||
if self._right_strategy is None:
|
|
||||||
self._right_strategy = session.config[
|
|
||||||
'right_strategy']
|
|
||||||
|
|
||||||
if self._user:
|
if self._user:
|
||||||
current_selection = utils.get_selected_objects(
|
current_selection = utils.get_selected_objects(
|
||||||
bpy.context.scene,
|
bpy.context.scene,
|
||||||
bpy.data.window_managers['WinMan'].windows[0].view_layer
|
bpy.data.window_managers['WinMan'].windows[0].view_layer
|
||||||
)
|
)
|
||||||
if current_selection != self._last_selection:
|
if current_selection != self._last_selection:
|
||||||
if self._right_strategy == RP_COMMON:
|
obj_common = [
|
||||||
obj_common = [
|
o for o in self._last_selection if o not in current_selection]
|
||||||
o for o in self._last_selection if o not in current_selection]
|
obj_ours = [
|
||||||
obj_ours = [
|
o for o in current_selection if o not in self._last_selection]
|
||||||
o for o in current_selection if o not in self._last_selection]
|
|
||||||
|
|
||||||
# change old selection right to common
|
# change old selection right to common
|
||||||
for obj in obj_common:
|
for obj in obj_common:
|
||||||
node = session.get(uuid=obj)
|
node = session.get(uuid=obj)
|
||||||
|
|
||||||
if node and (node.owner == settings.username or node.owner == RP_COMMON):
|
if node and (node.owner == settings.username or node.owner == RP_COMMON):
|
||||||
recursive = True
|
recursive = True
|
||||||
if node.data and 'instance_type' in node.data.keys():
|
if node.data and 'instance_type' in node.data.keys():
|
||||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||||
session.change_owner(
|
session.change_owner(
|
||||||
node.uuid,
|
node.uuid,
|
||||||
RP_COMMON,
|
RP_COMMON,
|
||||||
recursive=recursive)
|
recursive=recursive)
|
||||||
|
|
||||||
# change new selection to our
|
# change new selection to our
|
||||||
for obj in obj_ours:
|
for obj in obj_ours:
|
||||||
node = session.get(uuid=obj)
|
node = session.get(uuid=obj)
|
||||||
|
|
||||||
if node and node.owner == RP_COMMON:
|
if node and node.owner == RP_COMMON:
|
||||||
recursive = True
|
recursive = True
|
||||||
if node.data and 'instance_type' in node.data.keys():
|
if node.data and 'instance_type' in node.data.keys():
|
||||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||||
|
|
||||||
session.change_owner(
|
session.change_owner(
|
||||||
node.uuid,
|
node.uuid,
|
||||||
settings.username,
|
settings.username,
|
||||||
recursive=recursive)
|
recursive=recursive)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._last_selection = current_selection
|
self._last_selection = current_selection
|
||||||
|
|
||||||
user_metadata = {
|
user_metadata = {
|
||||||
'selected_objects': current_selection
|
'selected_objects': current_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
session.update_user_metadata(user_metadata)
|
session.update_user_metadata(user_metadata)
|
||||||
logging.debug("Update selection")
|
logging.debug("Update selection")
|
||||||
|
|
||||||
# Fix deselection until right managment refactoring (with Roles concepts)
|
# Fix deselection until right managment refactoring (with Roles concepts)
|
||||||
if len(current_selection) == 0 and self._right_strategy == RP_COMMON:
|
if len(current_selection) == 0 and self._right_strategy == RP_COMMON:
|
||||||
owned_keys = session.list(
|
owned_keys = session.list(
|
||||||
filter_owner=settings.username)
|
filter_owner=settings.username)
|
||||||
for key in owned_keys:
|
for key in owned_keys:
|
||||||
node = session.get(uuid=key)
|
node = session.get(uuid=key)
|
||||||
|
|
||||||
session.change_owner(
|
session.change_owner(
|
||||||
key,
|
key,
|
||||||
RP_COMMON,
|
RP_COMMON,
|
||||||
recursive=recursive)
|
recursive=recursive)
|
||||||
|
|
||||||
for user, user_info in session.online_users.items():
|
for user, user_info in session.online_users.items():
|
||||||
if user != settings.username:
|
if user != settings.username:
|
||||||
@ -210,12 +212,12 @@ class DrawClient(Draw):
|
|||||||
session = getattr(operators, 'client', None)
|
session = getattr(operators, 'client', None)
|
||||||
renderer = getattr(presence, 'renderer', None)
|
renderer = getattr(presence, 'renderer', None)
|
||||||
prefs = utils.get_preferences()
|
prefs = utils.get_preferences()
|
||||||
|
|
||||||
if session and renderer and session.state['STATE'] == STATE_ACTIVE:
|
if session and renderer and session.state['STATE'] == STATE_ACTIVE:
|
||||||
settings = bpy.context.window_manager.session
|
settings = bpy.context.window_manager.session
|
||||||
users = session.online_users
|
users = session.online_users
|
||||||
|
|
||||||
# Update users
|
# Update users
|
||||||
for user in users.values():
|
for user in users.values():
|
||||||
metadata = user.get('metadata')
|
metadata = user.get('metadata')
|
||||||
color = metadata.get('color')
|
color = metadata.get('color')
|
||||||
@ -229,14 +231,14 @@ class DrawClient(Draw):
|
|||||||
renderer.draw_client_camera(
|
renderer.draw_client_camera(
|
||||||
user['id'], metadata['view_corners'], color)
|
user['id'], metadata['view_corners'], color)
|
||||||
if not user_showable:
|
if not user_showable:
|
||||||
# TODO: remove this when user event drivent update will be
|
# TODO: remove this when user event drivent update will be
|
||||||
# ready
|
# ready
|
||||||
renderer.flush_selection()
|
renderer.flush_selection()
|
||||||
renderer.flush_users()
|
renderer.flush_users()
|
||||||
|
|
||||||
|
|
||||||
class ClientUpdate(Timer):
|
class ClientUpdate(Timer):
|
||||||
def __init__(self, timout=.016):
|
def __init__(self, timout=.032):
|
||||||
super().__init__(timout)
|
super().__init__(timout)
|
||||||
self.handle_quit = False
|
self.handle_quit = False
|
||||||
self.users_metadata = {}
|
self.users_metadata = {}
|
||||||
@ -245,15 +247,11 @@ class ClientUpdate(Timer):
|
|||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
session = getattr(operators, 'client', None)
|
session = getattr(operators, 'client', None)
|
||||||
renderer = getattr(presence, 'renderer', None)
|
renderer = getattr(presence, 'renderer', None)
|
||||||
|
|
||||||
if session and renderer:
|
|
||||||
if session.state['STATE'] == STATE_ACTIVE:
|
|
||||||
# Check if session has been closes prematurely
|
|
||||||
if session.state['STATE'] == 0:
|
|
||||||
bpy.ops.session.stop()
|
|
||||||
|
|
||||||
|
if session and renderer:
|
||||||
|
if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]:
|
||||||
local_user = operators.client.online_users.get(settings.username)
|
local_user = operators.client.online_users.get(settings.username)
|
||||||
|
|
||||||
if not local_user:
|
if not local_user:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@ -261,7 +259,7 @@ class ClientUpdate(Timer):
|
|||||||
if username != settings.username:
|
if username != settings.username:
|
||||||
cached_user_data = self.users_metadata.get(username)
|
cached_user_data = self.users_metadata.get(username)
|
||||||
new_user_data = operators.client.online_users[username]['metadata']
|
new_user_data = operators.client.online_users[username]['metadata']
|
||||||
|
|
||||||
if cached_user_data is None:
|
if cached_user_data is None:
|
||||||
self.users_metadata[username] = user_data['metadata']
|
self.users_metadata[username] = user_data['metadata']
|
||||||
elif 'view_matrix' in cached_user_data and 'view_matrix' in new_user_data and cached_user_data['view_matrix'] != new_user_data['view_matrix']:
|
elif 'view_matrix' in cached_user_data and 'view_matrix' in new_user_data and cached_user_data['view_matrix'] != new_user_data['view_matrix']:
|
||||||
@ -275,7 +273,7 @@ class ClientUpdate(Timer):
|
|||||||
scene_current = bpy.context.scene.name
|
scene_current = bpy.context.scene.name
|
||||||
local_user = session.online_users.get(settings.username)
|
local_user = session.online_users.get(settings.username)
|
||||||
current_view_corners = presence.get_view_corners()
|
current_view_corners = presence.get_view_corners()
|
||||||
|
|
||||||
# Init client metadata
|
# Init client metadata
|
||||||
if not local_user_metadata or 'color' not in local_user_metadata.keys():
|
if not local_user_metadata or 'color' not in local_user_metadata.keys():
|
||||||
metadata = {
|
metadata = {
|
||||||
@ -285,7 +283,7 @@ class ClientUpdate(Timer):
|
|||||||
settings.client_color.g,
|
settings.client_color.g,
|
||||||
settings.client_color.b,
|
settings.client_color.b,
|
||||||
1),
|
1),
|
||||||
'frame_current':bpy.context.scene.frame_current,
|
'frame_current': bpy.context.scene.frame_current,
|
||||||
'scene_current': scene_current
|
'scene_current': scene_current
|
||||||
}
|
}
|
||||||
session.update_user_metadata(metadata)
|
session.update_user_metadata(metadata)
|
||||||
@ -294,36 +292,41 @@ class ClientUpdate(Timer):
|
|||||||
# Update client current scene
|
# Update client current scene
|
||||||
elif scene_current != local_user_metadata['scene_current']:
|
elif scene_current != local_user_metadata['scene_current']:
|
||||||
local_user_metadata['scene_current'] = scene_current
|
local_user_metadata['scene_current'] = scene_current
|
||||||
session.update_user_metadata(local_user_metadata)
|
session.update_user_metadata(local_user_metadata)
|
||||||
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']:
|
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']:
|
||||||
local_user_metadata['view_corners'] = current_view_corners
|
local_user_metadata['view_corners'] = current_view_corners
|
||||||
local_user_metadata['view_matrix'] = presence.get_view_matrix()
|
local_user_metadata['view_matrix'] = presence.get_view_matrix()
|
||||||
session.update_user_metadata(local_user_metadata)
|
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):
|
class SessionStatusUpdate(Timer):
|
||||||
if user.username not in session_users.keys():
|
def __init__(self, timout=1):
|
||||||
ui_users.remove(index)
|
super().__init__(timout)
|
||||||
renderer.flush_selection()
|
|
||||||
renderer.flush_users()
|
|
||||||
break
|
|
||||||
|
|
||||||
for user in session_users:
|
def execute(self):
|
||||||
if user not in ui_users:
|
presence.refresh_sidebar_view()
|
||||||
new_key = ui_users.add()
|
|
||||||
new_key.name = user
|
class SessionUserSync(Timer):
|
||||||
new_key.username = user
|
def __init__(self, timout=1):
|
||||||
elif session.state['STATE'] == STATE_QUITTING:
|
super().__init__(timout)
|
||||||
presence.refresh_sidebar_view()
|
|
||||||
self.handle_quit = True
|
def execute(self):
|
||||||
elif session.state['STATE'] == STATE_INITIAL and self.handle_quit:
|
session = getattr(operators, 'client', None)
|
||||||
self.handle_quit = False
|
renderer = getattr(presence, 'renderer', None)
|
||||||
presence.refresh_sidebar_view()
|
|
||||||
|
if session and renderer:
|
||||||
operators.unregister_delayables()
|
# sync online users
|
||||||
|
session_users = operators.client.online_users
|
||||||
presence.renderer.stop()
|
ui_users = bpy.context.window_manager.online_users
|
||||||
|
|
||||||
presence.refresh_sidebar_view()
|
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
|
@ -22,16 +22,21 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import socket
|
||||||
|
import re
|
||||||
|
|
||||||
|
VERSION_EXPR = re.compile('\d+\.\d+\.\d+')
|
||||||
|
|
||||||
THIRD_PARTY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libs")
|
THIRD_PARTY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libs")
|
||||||
DEFAULT_CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache")
|
DEFAULT_CACHE_DIR = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "cache")
|
||||||
PYTHON_PATH = None
|
PYTHON_PATH = None
|
||||||
SUBPROCESS_DIR = None
|
SUBPROCESS_DIR = None
|
||||||
|
|
||||||
|
|
||||||
rtypes = []
|
rtypes = []
|
||||||
|
|
||||||
|
|
||||||
def module_can_be_imported(name):
|
def module_can_be_imported(name):
|
||||||
try:
|
try:
|
||||||
__import__(name)
|
__import__(name)
|
||||||
@ -42,19 +47,43 @@ def module_can_be_imported(name):
|
|||||||
|
|
||||||
def install_pip():
|
def install_pip():
|
||||||
# pip can not necessarily be imported into Blender after this
|
# pip can not necessarily be imported into Blender after this
|
||||||
get_pip_path = Path(__file__).parent / "libs" / "get-pip.py"
|
subprocess.run([str(PYTHON_PATH), "-m", "ensurepip"])
|
||||||
subprocess.run([str(PYTHON_PATH), str(get_pip_path)], cwd=SUBPROCESS_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
def install_package(name):
|
def install_package(name, version):
|
||||||
logging.debug(f"Using {PYTHON_PATH} for installation")
|
logging.info(f"installing {name} version...")
|
||||||
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", name])
|
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", f"{name}=={version}"])
|
||||||
|
|
||||||
|
def check_package_version(name, required_version):
|
||||||
|
logging.info(f"Checking {name} version...")
|
||||||
|
out = subprocess.run(f"{str(PYTHON_PATH)} -m pip show {name}", capture_output=True)
|
||||||
|
|
||||||
|
version = VERSION_EXPR.search(out.stdout.decode())
|
||||||
|
|
||||||
|
if version and version.group() == required_version:
|
||||||
|
logging.info(f"{name} is up to date")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.info(f"{name} need an update")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_ip():
|
||||||
|
"""
|
||||||
|
Retrieve the main network interface IP.
|
||||||
|
|
||||||
|
"""
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("8.8.8.8", 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
return ip
|
||||||
|
|
||||||
|
|
||||||
def check_dir(dir):
|
def check_dir(dir):
|
||||||
if not os.path.exists(dir):
|
if not os.path.exists(dir):
|
||||||
os.makedirs(dir)
|
os.makedirs(dir)
|
||||||
|
|
||||||
|
|
||||||
def setup(dependencies, python_path):
|
def setup(dependencies, python_path):
|
||||||
global PYTHON_PATH, SUBPROCESS_DIR
|
global PYTHON_PATH, SUBPROCESS_DIR
|
||||||
|
|
||||||
@ -64,7 +93,9 @@ def setup(dependencies, python_path):
|
|||||||
if not module_can_be_imported("pip"):
|
if not module_can_be_imported("pip"):
|
||||||
install_pip()
|
install_pip()
|
||||||
|
|
||||||
for module_name, package_name in dependencies:
|
for package_name, package_version in dependencies:
|
||||||
if not module_can_be_imported(module_name):
|
if not module_can_be_imported(package_name):
|
||||||
install_package(package_name)
|
install_package(package_name, package_version)
|
||||||
module_can_be_imported(package_name)
|
module_can_be_imported(package_name)
|
||||||
|
elif not check_package_version(package_name, package_version):
|
||||||
|
install_package(package_name, package_version)
|
||||||
|
@ -26,38 +26,26 @@ import time
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE, Popen, TimeoutExpired
|
from subprocess import PIPE, Popen, TimeoutExpired
|
||||||
|
import zmq
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import mathutils
|
import mathutils
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
from . import bl_types, delayable, environment, presence, ui, utils
|
from . import bl_types, delayable, environment, presence, ui, utils
|
||||||
from .libs.replication.replication.constants import (FETCHED, STATE_ACTIVE,
|
from replication.constants import (FETCHED, STATE_ACTIVE,
|
||||||
STATE_INITIAL,
|
STATE_INITIAL,
|
||||||
STATE_SYNCING,UP)
|
STATE_SYNCING)
|
||||||
from .libs.replication.replication.data import ReplicatedDataFactory
|
from replication.data import ReplicatedDataFactory
|
||||||
from .libs.replication.replication.exception import NonAuthorizedOperationError
|
from replication.exception import NonAuthorizedOperationError
|
||||||
from .libs.replication.replication.interface import Session
|
from replication.interface import Session
|
||||||
|
|
||||||
|
|
||||||
client = None
|
client = None
|
||||||
delayables = []
|
delayables = []
|
||||||
ui_context = None
|
|
||||||
stop_modal_executor = False
|
stop_modal_executor = False
|
||||||
modal_executor_queue = None
|
|
||||||
server_process = None
|
|
||||||
|
|
||||||
def unregister_delayables():
|
|
||||||
global delayables, stop_modal_executor
|
|
||||||
|
|
||||||
for d in delayables:
|
|
||||||
try:
|
|
||||||
d.unregister()
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
stop_modal_executor = True
|
|
||||||
|
|
||||||
# OPERATORS
|
# OPERATORS
|
||||||
|
|
||||||
|
|
||||||
@ -73,18 +61,18 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
global client, delayables, ui_context, server_process
|
global client, delayables
|
||||||
|
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
runtime_settings = context.window_manager.session
|
runtime_settings = context.window_manager.session
|
||||||
users = bpy.data.window_managers['WinMan'].online_users
|
users = bpy.data.window_managers['WinMan'].online_users
|
||||||
|
admin_pass = runtime_settings.password
|
||||||
|
|
||||||
# TODO: Sync server clients
|
|
||||||
users.clear()
|
users.clear()
|
||||||
delayables.clear()
|
delayables.clear()
|
||||||
|
|
||||||
bpy_factory = ReplicatedDataFactory()
|
bpy_factory = ReplicatedDataFactory()
|
||||||
supported_bl_types = []
|
supported_bl_types = []
|
||||||
ui_context = context.copy()
|
|
||||||
|
|
||||||
# init the factory with supported types
|
# init the factory with supported types
|
||||||
for type in bl_types.types_to_register():
|
for type in bl_types.types_to_register():
|
||||||
@ -105,38 +93,39 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
client = Session(
|
client = Session(
|
||||||
factory=bpy_factory,
|
factory=bpy_factory,
|
||||||
python_path=bpy.app.binary_path_python,
|
python_path=bpy.app.binary_path_python)
|
||||||
default_strategy=settings.right_strategy)
|
|
||||||
|
|
||||||
delayables.append(delayable.ApplyTimer())
|
delayables.append(delayable.ApplyTimer())
|
||||||
|
|
||||||
# Host a session
|
# Host a session
|
||||||
if self.host:
|
if self.host:
|
||||||
# Scene setup
|
if settings.init_method == 'EMPTY':
|
||||||
if settings.start_empty:
|
|
||||||
utils.clean_scene()
|
utils.clean_scene()
|
||||||
|
|
||||||
try:
|
runtime_settings.is_host = True
|
||||||
for scene in bpy.data.scenes:
|
runtime_settings.internet_ip = environment.get_ip()
|
||||||
scene_uuid = client.add(scene)
|
|
||||||
client.commit(scene_uuid)
|
|
||||||
|
|
||||||
|
for scene in bpy.data.scenes:
|
||||||
|
client.add(scene)
|
||||||
|
|
||||||
|
try:
|
||||||
client.host(
|
client.host(
|
||||||
id=settings.username,
|
id=settings.username,
|
||||||
address=settings.ip,
|
|
||||||
port=settings.port,
|
port=settings.port,
|
||||||
ipc_port=settings.ipc_port,
|
ipc_port=settings.ipc_port,
|
||||||
timeout=settings.connection_timeout
|
timeout=settings.connection_timeout,
|
||||||
|
password=admin_pass
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
logging.error(f"Error: {e}")
|
logging.error(f"Error: {e}")
|
||||||
finally:
|
|
||||||
runtime_settings.is_admin = True
|
|
||||||
|
|
||||||
# Join a session
|
# Join a session
|
||||||
else:
|
else:
|
||||||
utils.clean_scene()
|
if not runtime_settings.admin:
|
||||||
|
utils.clean_scene()
|
||||||
|
# regular client, no password needed
|
||||||
|
admin_pass = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client.connect(
|
client.connect(
|
||||||
@ -144,13 +133,12 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
address=settings.ip,
|
address=settings.ip,
|
||||||
port=settings.port,
|
port=settings.port,
|
||||||
ipc_port=settings.ipc_port,
|
ipc_port=settings.ipc_port,
|
||||||
timeout=settings.connection_timeout
|
timeout=settings.connection_timeout,
|
||||||
|
password=admin_pass
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, str(e))
|
||||||
logging.error(f"Error: {e}")
|
logging.error(str(e))
|
||||||
finally:
|
|
||||||
runtime_settings.is_admin = False
|
|
||||||
|
|
||||||
# Background client updates service
|
# Background client updates service
|
||||||
#TODO: Refactoring
|
#TODO: Refactoring
|
||||||
@ -158,16 +146,44 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
delayables.append(delayable.DrawClient())
|
delayables.append(delayable.DrawClient())
|
||||||
delayables.append(delayable.DynamicRightSelectTimer())
|
delayables.append(delayable.DynamicRightSelectTimer())
|
||||||
|
|
||||||
# Launch drawing module
|
session_update = delayable.SessionStatusUpdate()
|
||||||
if runtime_settings.enable_presence:
|
session_user_sync = delayable.SessionUserSync()
|
||||||
presence.renderer.run()
|
session_update.register()
|
||||||
|
session_user_sync.register()
|
||||||
|
|
||||||
# Register blender main thread tools
|
delayables.append(session_update)
|
||||||
for d in delayables:
|
delayables.append(session_user_sync)
|
||||||
d.register()
|
|
||||||
|
|
||||||
|
@client.register('on_connection')
|
||||||
|
def initialize_session():
|
||||||
|
for node in client._graph.list_ordered():
|
||||||
|
node_ref = client.get(node)
|
||||||
|
if node_ref.state == FETCHED:
|
||||||
|
node_ref.resolve()
|
||||||
|
node_ref.apply()
|
||||||
|
|
||||||
|
# Launch drawing module
|
||||||
|
if runtime_settings.enable_presence:
|
||||||
|
presence.renderer.run()
|
||||||
|
|
||||||
|
# Register blender main thread tools
|
||||||
|
for d in delayables:
|
||||||
|
d.register()
|
||||||
|
|
||||||
|
@client.register('on_exit')
|
||||||
|
def desinitialize_session():
|
||||||
|
global delayables, stop_modal_executor
|
||||||
|
|
||||||
|
for d in delayables:
|
||||||
|
try:
|
||||||
|
d.unregister()
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
stop_modal_executor = True
|
||||||
|
presence.renderer.stop()
|
||||||
|
|
||||||
global modal_executor_queue
|
|
||||||
modal_executor_queue = queue.Queue()
|
|
||||||
bpy.ops.session.apply_armature_operator()
|
bpy.ops.session.apply_armature_operator()
|
||||||
|
|
||||||
self.report(
|
self.report(
|
||||||
@ -176,6 +192,47 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class SessionInitOperator(bpy.types.Operator):
|
||||||
|
bl_idname = "session.init"
|
||||||
|
bl_label = "Init session repostitory from"
|
||||||
|
bl_description = "Init the current session"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
init_method: bpy.props.EnumProperty(
|
||||||
|
name='init_method',
|
||||||
|
description='Init repo',
|
||||||
|
items={
|
||||||
|
('EMPTY', 'an empty scene', 'start empty'),
|
||||||
|
('BLEND', 'current scenes', 'use current scenes')},
|
||||||
|
default='BLEND')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
col = layout.column()
|
||||||
|
col.prop(self, 'init_method', text="")
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
wm = context.window_manager
|
||||||
|
return wm.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
global client
|
||||||
|
|
||||||
|
if self.init_method == 'EMPTY':
|
||||||
|
utils.clean_scene()
|
||||||
|
|
||||||
|
for scene in bpy.data.scenes:
|
||||||
|
client.add(scene)
|
||||||
|
|
||||||
|
client.init()
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class SessionStopOperator(bpy.types.Operator):
|
class SessionStopOperator(bpy.types.Operator):
|
||||||
bl_idname = "session.stop"
|
bl_idname = "session.stop"
|
||||||
bl_label = "close"
|
bl_label = "close"
|
||||||
@ -188,15 +245,18 @@ class SessionStopOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
global client, delayables, stop_modal_executor
|
global client, delayables, stop_modal_executor
|
||||||
assert(client)
|
|
||||||
|
|
||||||
try:
|
|
||||||
client.disconnect()
|
|
||||||
except Exception as e:
|
|
||||||
self.report({'ERROR'}, repr(e))
|
|
||||||
|
|
||||||
|
if client:
|
||||||
|
try:
|
||||||
|
client.disconnect()
|
||||||
|
except Exception as e:
|
||||||
|
self.report({'ERROR'}, repr(e))
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, "No session to quit.")
|
||||||
|
return {"FINISHED"}
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class SessionKickOperator(bpy.types.Operator):
|
class SessionKickOperator(bpy.types.Operator):
|
||||||
bl_idname = "session.kick"
|
bl_idname = "session.kick"
|
||||||
bl_label = "Kick"
|
bl_label = "Kick"
|
||||||
@ -211,7 +271,7 @@ class SessionKickOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
global client, delayables, stop_modal_executor
|
global client, delayables, stop_modal_executor
|
||||||
assert(client)
|
assert(client)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client.kick(self.user)
|
client.kick(self.user)
|
||||||
@ -222,11 +282,11 @@ class SessionKickOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
row = self.layout
|
row = self.layout
|
||||||
row.label(text=f" Do you really want to kick {self.user} ? " )
|
row.label(text=f" Do you really want to kick {self.user} ? ")
|
||||||
|
|
||||||
|
|
||||||
class SessionPropertyRemoveOperator(bpy.types.Operator):
|
class SessionPropertyRemoveOperator(bpy.types.Operator):
|
||||||
bl_idname = "session.remove_prop"
|
bl_idname = "session.remove_prop"
|
||||||
@ -319,7 +379,8 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
|||||||
wm.event_timer_remove(self._timer)
|
wm.event_timer_remove(self._timer)
|
||||||
|
|
||||||
def modal(self, context, event):
|
def modal(self, context, event):
|
||||||
is_running = context.window_manager.session.time_snap_running
|
session_sessings = context.window_manager.session
|
||||||
|
is_running = session_sessings.time_snap_running
|
||||||
|
|
||||||
if event.type in {'RIGHTMOUSE', 'ESC'} or not is_running:
|
if event.type in {'RIGHTMOUSE', 'ESC'} or not is_running:
|
||||||
self.cancel(context)
|
self.cancel(context)
|
||||||
@ -334,11 +395,26 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
if target_ref:
|
if target_ref:
|
||||||
target_scene = target_ref['metadata']['scene_current']
|
target_scene = target_ref['metadata']['scene_current']
|
||||||
if target_scene != context.scene.name:
|
|
||||||
bpy.context.window.scene = bpy.data.scenes[target_scene]
|
|
||||||
|
|
||||||
rv3d.view_matrix = mathutils.Matrix(
|
# Handle client on other scenes
|
||||||
target_ref['metadata']['view_matrix'])
|
if target_scene != context.scene.name:
|
||||||
|
blender_scene = bpy.data.scenes.get(target_scene, None)
|
||||||
|
if blender_scene is None:
|
||||||
|
self.report({'ERROR'}, f"Scene {target_scene} doesn't exist on the local client.")
|
||||||
|
session_sessings.time_snap_running = False
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
bpy.context.window.scene = blender_scene
|
||||||
|
|
||||||
|
# Update client viewmatrix
|
||||||
|
client_vmatrix = target_ref['metadata'].get('view_matrix', None)
|
||||||
|
|
||||||
|
if client_vmatrix:
|
||||||
|
rv3d.view_matrix = mathutils.Matrix(client_vmatrix)
|
||||||
|
else:
|
||||||
|
self.report({'ERROR'}, f"Client viewport not ready.")
|
||||||
|
session_sessings.time_snap_running = False
|
||||||
|
return {"CANCELLED"}
|
||||||
else:
|
else:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
@ -462,7 +538,7 @@ class ApplyArmatureOperator(bpy.types.Operator):
|
|||||||
try:
|
try:
|
||||||
client.apply(node)
|
client.apply(node)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Dail to apply armature: {e}")
|
logging.error("Fail to apply armature: {e}")
|
||||||
|
|
||||||
return {'PASS_THROUGH'}
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
@ -492,10 +568,26 @@ classes = (
|
|||||||
SessionCommit,
|
SessionCommit,
|
||||||
ApplyArmatureOperator,
|
ApplyArmatureOperator,
|
||||||
SessionKickOperator,
|
SessionKickOperator,
|
||||||
|
SessionInitOperator,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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'] == STATE_ACTIVE:
|
||||||
|
for node_key in client.list():
|
||||||
|
client.get(node_key).resolve()
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def load_pre_handler(dummy):
|
def load_pre_handler(dummy):
|
||||||
global client
|
global client
|
||||||
@ -504,9 +596,6 @@ def load_pre_handler(dummy):
|
|||||||
bpy.ops.session.stop()
|
bpy.ops.session.stop()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def update_client_frame(scene):
|
def update_client_frame(scene):
|
||||||
if client and client.state['STATE'] == STATE_ACTIVE:
|
if client and client.state['STATE'] == STATE_ACTIVE:
|
||||||
@ -553,9 +642,10 @@ def register():
|
|||||||
for cls in classes:
|
for cls in classes:
|
||||||
register_class(cls)
|
register_class(cls)
|
||||||
|
|
||||||
|
bpy.app.handlers.undo_post.append(sanitize_deps_graph)
|
||||||
|
bpy.app.handlers.redo_post.append(sanitize_deps_graph)
|
||||||
|
|
||||||
bpy.app.handlers.load_pre.append(load_pre_handler)
|
bpy.app.handlers.load_pre.append(load_pre_handler)
|
||||||
|
|
||||||
|
|
||||||
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
||||||
|
|
||||||
bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
|
bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
|
||||||
@ -572,8 +662,9 @@ def unregister():
|
|||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
unregister_class(cls)
|
unregister_class(cls)
|
||||||
|
|
||||||
|
bpy.app.handlers.undo_post.remove(sanitize_deps_graph)
|
||||||
|
bpy.app.handlers.redo_post.remove(sanitize_deps_graph)
|
||||||
|
|
||||||
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
||||||
|
|
||||||
|
|
||||||
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
||||||
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation)
|
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation)
|
@ -19,10 +19,12 @@ import random
|
|||||||
import logging
|
import logging
|
||||||
import bpy
|
import bpy
|
||||||
import string
|
import string
|
||||||
|
import re
|
||||||
|
|
||||||
from . import utils, bl_types, environment, addon_updater_ops, presence, ui
|
from . import utils, bl_types, environment, addon_updater_ops, presence, ui
|
||||||
from .libs.replication.replication.constants import RP_COMMON
|
from replication.constants import RP_COMMON
|
||||||
|
|
||||||
|
IP_EXPR = re.compile('\d+\.\d+\.\d+\.\d+')
|
||||||
|
|
||||||
|
|
||||||
def randomColor():
|
def randomColor():
|
||||||
@ -44,6 +46,22 @@ def update_panel_category(self, context):
|
|||||||
ui.SESSION_PT_settings.bl_category = self.panel_category
|
ui.SESSION_PT_settings.bl_category = self.panel_category
|
||||||
ui.register()
|
ui.register()
|
||||||
|
|
||||||
|
def update_ip(self, context):
|
||||||
|
ip = IP_EXPR.search(self.ip)
|
||||||
|
|
||||||
|
if ip:
|
||||||
|
self['ip'] = ip.group()
|
||||||
|
else:
|
||||||
|
logging.error("Wrong IP format")
|
||||||
|
self['ip'] = "127.0.0.1"
|
||||||
|
|
||||||
|
def update_port(self, context):
|
||||||
|
max_port = self.port + 3
|
||||||
|
|
||||||
|
if self.ipc_port < max_port and \
|
||||||
|
self['ipc_port'] >= self.port:
|
||||||
|
logging.error("IPC Port in conflic with the port, assigning a random value")
|
||||||
|
self['ipc_port'] = random.randrange(self.port+4, 10000)
|
||||||
|
|
||||||
class ReplicatedDatablock(bpy.types.PropertyGroup):
|
class ReplicatedDatablock(bpy.types.PropertyGroup):
|
||||||
type_name: bpy.props.StringProperty()
|
type_name: bpy.props.StringProperty()
|
||||||
@ -68,7 +86,8 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
ip: bpy.props.StringProperty(
|
ip: bpy.props.StringProperty(
|
||||||
name="ip",
|
name="ip",
|
||||||
description='Distant host ip',
|
description='Distant host ip',
|
||||||
default="127.0.0.1")
|
default="127.0.0.1",
|
||||||
|
update=update_ip)
|
||||||
username: bpy.props.StringProperty(
|
username: bpy.props.StringProperty(
|
||||||
name="Username",
|
name="Username",
|
||||||
default=f"user_{random_string_digits()}"
|
default=f"user_{random_string_digits()}"
|
||||||
@ -91,19 +110,16 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
ipc_port: bpy.props.IntProperty(
|
ipc_port: bpy.props.IntProperty(
|
||||||
name="ipc_port",
|
name="ipc_port",
|
||||||
description='internal ttl port(only usefull for multiple local instances)',
|
description='internal ttl port(only usefull for multiple local instances)',
|
||||||
default=5561
|
default=5561,
|
||||||
|
update=update_port
|
||||||
)
|
)
|
||||||
start_empty: bpy.props.BoolProperty(
|
init_method: bpy.props.EnumProperty(
|
||||||
name="start_empty",
|
name='init_method',
|
||||||
default=False
|
description='Init repo',
|
||||||
)
|
|
||||||
right_strategy: bpy.props.EnumProperty(
|
|
||||||
name='right_strategy',
|
|
||||||
description='right strategy',
|
|
||||||
items={
|
items={
|
||||||
('STRICT', 'strict', 'strict right repartition'),
|
('EMPTY', 'an empty scene', 'start empty'),
|
||||||
('COMMON', 'common', 'relaxed right repartition')},
|
('BLEND', 'current scenes', 'use current scenes')},
|
||||||
default='COMMON')
|
default='BLEND')
|
||||||
cache_directory: bpy.props.StringProperty(
|
cache_directory: bpy.props.StringProperty(
|
||||||
name="cache directory",
|
name="cache directory",
|
||||||
subtype="DIR_PATH",
|
subtype="DIR_PATH",
|
||||||
@ -236,8 +252,8 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
row.label(text="Port:")
|
row.label(text="Port:")
|
||||||
row.prop(self, "port", text="Address")
|
row.prop(self, "port", text="Address")
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.label(text="Start with an empty scene:")
|
row.label(text="Init the session from:")
|
||||||
row.prop(self, "start_empty", text="")
|
row.prop(self, "init_method", text="")
|
||||||
|
|
||||||
table = box.box()
|
table = box.box()
|
||||||
table.row().prop(
|
table.row().prop(
|
||||||
@ -264,10 +280,9 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_hosting_expanded
|
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_hosting_expanded
|
||||||
else 'DISCLOSURE_TRI_RIGHT', emboss=False)
|
else 'DISCLOSURE_TRI_RIGHT', emboss=False)
|
||||||
if self.conf_session_hosting_expanded:
|
if self.conf_session_hosting_expanded:
|
||||||
box.row().prop(self, "right_strategy", text="Right model")
|
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.label(text="Start with an empty scene:")
|
row.label(text="Init the session from:")
|
||||||
row.prop(self, "start_empty", text="")
|
row.prop(self, "init_method", text="")
|
||||||
|
|
||||||
# CACHE SETTINGS
|
# CACHE SETTINGS
|
||||||
box = grid.box()
|
box = grid.box()
|
||||||
@ -340,17 +355,13 @@ class SessionUser(bpy.types.PropertyGroup):
|
|||||||
|
|
||||||
|
|
||||||
class SessionProps(bpy.types.PropertyGroup):
|
class SessionProps(bpy.types.PropertyGroup):
|
||||||
is_admin: bpy.props.BoolProperty(
|
|
||||||
name="is_admin",
|
|
||||||
default=False
|
|
||||||
)
|
|
||||||
session_mode: bpy.props.EnumProperty(
|
session_mode: bpy.props.EnumProperty(
|
||||||
name='session_mode',
|
name='session_mode',
|
||||||
description='session mode',
|
description='session mode',
|
||||||
items={
|
items={
|
||||||
('HOST', 'hosting', 'host a session'),
|
('HOST', 'HOST', 'host a session'),
|
||||||
('CONNECT', 'connexion', 'connect to a session')},
|
('CONNECT', 'JOIN', 'connect to a session')},
|
||||||
default='HOST')
|
default='CONNECT')
|
||||||
clients: bpy.props.EnumProperty(
|
clients: bpy.props.EnumProperty(
|
||||||
name="clients",
|
name="clients",
|
||||||
description="client enum",
|
description="client enum",
|
||||||
@ -374,7 +385,7 @@ class SessionProps(bpy.types.PropertyGroup):
|
|||||||
update=presence.update_overlay_settings
|
update=presence.update_overlay_settings
|
||||||
)
|
)
|
||||||
presence_show_far_user: bpy.props.BoolProperty(
|
presence_show_far_user: bpy.props.BoolProperty(
|
||||||
name="Show different scenes",
|
name="Show users on different scenes",
|
||||||
description="Show user on different scenes",
|
description="Show user on different scenes",
|
||||||
default=False,
|
default=False,
|
||||||
update=presence.update_overlay_settings
|
update=presence.update_overlay_settings
|
||||||
@ -384,12 +395,31 @@ class SessionProps(bpy.types.PropertyGroup):
|
|||||||
description='Show only owned datablocks',
|
description='Show only owned datablocks',
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
admin: bpy.props.BoolProperty(
|
||||||
|
name="admin",
|
||||||
|
description='Connect as admin',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
password: bpy.props.StringProperty(
|
||||||
|
name="password",
|
||||||
|
default=random_string_digits(),
|
||||||
|
description='Session password',
|
||||||
|
subtype='PASSWORD'
|
||||||
|
)
|
||||||
|
internet_ip: bpy.props.StringProperty(
|
||||||
|
name="internet ip",
|
||||||
|
default="no found",
|
||||||
|
description='Internet interface ip',
|
||||||
|
)
|
||||||
user_snap_running: bpy.props.BoolProperty(
|
user_snap_running: bpy.props.BoolProperty(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
time_snap_running: bpy.props.BoolProperty(
|
time_snap_running: bpy.props.BoolProperty(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
is_host: bpy.props.BoolProperty(
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import traceback
|
||||||
|
|
||||||
import bgl
|
import bgl
|
||||||
import blf
|
import blf
|
||||||
@ -311,10 +312,10 @@ class DrawFactory(object):
|
|||||||
self.d2d_items[client_id] = (position[1], client_id, color)
|
self.d2d_items[client_id] = (position[1], client_id, color)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Draw client exception: {e}")
|
logging.debug(f"Draw client exception: {e} \n {traceback.format_exc()}\n pos:{position},ind:{indices}")
|
||||||
|
|
||||||
def draw3d_callback(self):
|
def draw3d_callback(self):
|
||||||
bgl.glLineWidth(1.5)
|
bgl.glLineWidth(2.)
|
||||||
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
||||||
bgl.glEnable(bgl.GL_BLEND)
|
bgl.glEnable(bgl.GL_BLEND)
|
||||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||||
|
283
multi_user/ui.py
@ -19,12 +19,13 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from . import operators, utils
|
from . import operators, utils
|
||||||
from .libs.replication.replication.constants import (ADDED, ERROR, FETCHED,
|
from replication.constants import (ADDED, ERROR, FETCHED,
|
||||||
MODIFIED, RP_COMMON, UP,
|
MODIFIED, RP_COMMON, UP,
|
||||||
STATE_ACTIVE, STATE_AUTH,
|
STATE_ACTIVE, STATE_AUTH,
|
||||||
STATE_CONFIG, STATE_SYNCING,
|
STATE_CONFIG, STATE_SYNCING,
|
||||||
STATE_INITIAL, STATE_SRV_SYNC,
|
STATE_INITIAL, STATE_SRV_SYNC,
|
||||||
STATE_WAITING, STATE_QUITTING,
|
STATE_WAITING, STATE_QUITTING,
|
||||||
|
STATE_LOBBY,
|
||||||
STATE_LAUNCHING_SERVICES)
|
STATE_LAUNCHING_SERVICES)
|
||||||
|
|
||||||
ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
|
ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
|
||||||
@ -34,7 +35,8 @@ ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
|
|||||||
'FILE_REFRESH', # UP
|
'FILE_REFRESH', # UP
|
||||||
'TRIA_UP'] # CHANGED
|
'TRIA_UP'] # CHANGED
|
||||||
|
|
||||||
def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█', fill_empty=' '):
|
|
||||||
|
def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█', fill_empty=' '):
|
||||||
"""
|
"""
|
||||||
Call in a loop to create terminal progress bar
|
Call in a loop to create terminal progress bar
|
||||||
@params:
|
@params:
|
||||||
@ -48,16 +50,19 @@ def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1,
|
|||||||
From here:
|
From here:
|
||||||
https://gist.github.com/greenstick/b23e475d2bfdc3a82e34eaa1f6781ee4
|
https://gist.github.com/greenstick/b23e475d2bfdc3a82e34eaa1f6781ee4
|
||||||
"""
|
"""
|
||||||
|
if total == 0:
|
||||||
|
return ""
|
||||||
filledLength = int(length * iteration // total)
|
filledLength = int(length * iteration // total)
|
||||||
bar = fill * filledLength + fill_empty * (length - filledLength)
|
bar = fill * filledLength + fill_empty * (length - filledLength)
|
||||||
return f"{prefix} |{bar}| {iteration}/{total}{suffix}"
|
return f"{prefix} |{bar}| {iteration}/{total}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
def get_state_str(state):
|
def get_state_str(state):
|
||||||
state_str = 'UNKNOWN'
|
state_str = 'UNKNOWN'
|
||||||
if state == STATE_WAITING:
|
if state == STATE_WAITING:
|
||||||
state_str = 'WARMING UP DATA'
|
state_str = 'WARMING UP DATA'
|
||||||
elif state == STATE_SYNCING:
|
elif state == STATE_SYNCING:
|
||||||
state_str = 'FETCHING FROM SERVER'
|
state_str = 'FETCHING'
|
||||||
elif state == STATE_AUTH:
|
elif state == STATE_AUTH:
|
||||||
state_str = 'AUTHENTIFICATION'
|
state_str = 'AUTHENTIFICATION'
|
||||||
elif state == STATE_CONFIG:
|
elif state == STATE_CONFIG:
|
||||||
@ -65,31 +70,49 @@ def get_state_str(state):
|
|||||||
elif state == STATE_ACTIVE:
|
elif state == STATE_ACTIVE:
|
||||||
state_str = 'ONLINE'
|
state_str = 'ONLINE'
|
||||||
elif state == STATE_SRV_SYNC:
|
elif state == STATE_SRV_SYNC:
|
||||||
state_str = 'PUSHING TO SERVER'
|
state_str = 'PUSHING'
|
||||||
elif state == STATE_INITIAL:
|
elif state == STATE_INITIAL:
|
||||||
state_str = 'INIT'
|
state_str = 'INIT'
|
||||||
elif state == STATE_QUITTING:
|
elif state == STATE_QUITTING:
|
||||||
state_str = 'QUITTING SESSION'
|
state_str = 'QUITTING'
|
||||||
elif state == STATE_LAUNCHING_SERVICES:
|
elif state == STATE_LAUNCHING_SERVICES:
|
||||||
state_str = 'LAUNCHING SERVICES'
|
state_str = 'LAUNCHING SERVICES'
|
||||||
|
elif state == STATE_LOBBY:
|
||||||
|
state_str = 'LOBBY'
|
||||||
|
|
||||||
return state_str
|
return state_str
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_settings(bpy.types.Panel):
|
class SESSION_PT_settings(bpy.types.Panel):
|
||||||
"""Settings panel"""
|
"""Settings panel"""
|
||||||
bl_idname = "MULTIUSER_SETTINGS_PT_panel"
|
bl_idname = "MULTIUSER_SETTINGS_PT_panel"
|
||||||
bl_label = "Session"
|
bl_label = " "
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_category = "Multiuser"
|
bl_category = "Multiuser"
|
||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
self.layout.label(text="", icon='TOOL_SETTINGS')
|
layout = self.layout
|
||||||
|
if operators.client and operators.client.state['STATE'] != STATE_INITIAL:
|
||||||
|
cli_state = operators.client.state
|
||||||
|
state = operators.client.state.get('STATE')
|
||||||
|
connection_icon = "KEYTYPE_MOVING_HOLD_VEC"
|
||||||
|
|
||||||
|
if state == STATE_ACTIVE:
|
||||||
|
connection_icon = 'PROP_ON'
|
||||||
|
else:
|
||||||
|
connection_icon = 'PROP_CON'
|
||||||
|
|
||||||
|
layout.label(text=f"Session - {get_state_str(cli_state['STATE'])}", icon=connection_icon)
|
||||||
|
else:
|
||||||
|
layout.label(text="Session",icon="PROP_OFF")
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
|
runtime_settings = context.window_manager.session
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
if hasattr(context.window_manager, 'session'):
|
if hasattr(context.window_manager, 'session'):
|
||||||
# STATE INITIAL
|
# STATE INITIAL
|
||||||
@ -98,26 +121,33 @@ class SESSION_PT_settings(bpy.types.Panel):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
cli_state = operators.client.state
|
cli_state = operators.client.state
|
||||||
|
|
||||||
|
|
||||||
row.label(text=f"Status : {get_state_str(cli_state['STATE'])}")
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
|
|
||||||
current_state = cli_state['STATE']
|
current_state = cli_state['STATE']
|
||||||
|
|
||||||
# STATE ACTIVE
|
# STATE ACTIVE
|
||||||
if current_state == STATE_ACTIVE:
|
if current_state in [STATE_ACTIVE]:
|
||||||
row.operator("session.stop", icon='QUIT', text="Exit")
|
row.operator("session.stop", icon='QUIT', text="Exit")
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
|
if runtime_settings.is_host:
|
||||||
|
row = row.box()
|
||||||
|
row.label(text=f"LAN: {runtime_settings.internet_ip}", icon='INFO')
|
||||||
|
row = layout.row()
|
||||||
|
if current_state == STATE_LOBBY:
|
||||||
|
row = row.box()
|
||||||
|
row.label(text=f"Waiting the session to start", icon='INFO')
|
||||||
|
row = layout.row()
|
||||||
|
row.operator("session.stop", icon='QUIT', text="Exit")
|
||||||
# CONNECTION STATE
|
# CONNECTION STATE
|
||||||
elif current_state in [
|
elif current_state in [STATE_SRV_SYNC,
|
||||||
STATE_SRV_SYNC,
|
STATE_SYNCING,
|
||||||
STATE_SYNCING,
|
STATE_AUTH,
|
||||||
STATE_AUTH,
|
STATE_CONFIG,
|
||||||
STATE_CONFIG,
|
STATE_WAITING]:
|
||||||
STATE_WAITING]:
|
|
||||||
|
if cli_state['STATE'] in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]:
|
||||||
if cli_state['STATE'] in [STATE_SYNCING,STATE_SRV_SYNC,STATE_WAITING]:
|
|
||||||
box = row.box()
|
box = row.box()
|
||||||
box.label(text=printProgressBar(
|
box.label(text=printProgressBar(
|
||||||
cli_state['CURRENT'],
|
cli_state['CURRENT'],
|
||||||
@ -136,13 +166,15 @@ class SESSION_PT_settings(bpy.types.Panel):
|
|||||||
if state == STATE_ACTIVE:
|
if state == STATE_ACTIVE:
|
||||||
num_online_services += 1
|
num_online_services += 1
|
||||||
|
|
||||||
total_online_services = len(operators.client.services_state)
|
total_online_services = len(
|
||||||
|
operators.client.services_state)
|
||||||
|
|
||||||
box.label(text=printProgressBar(
|
box.label(text=printProgressBar(
|
||||||
total_online_services-num_online_services,
|
total_online_services-num_online_services,
|
||||||
total_online_services,
|
total_online_services,
|
||||||
length=16
|
length=16
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_settings_network(bpy.types.Panel):
|
class SESSION_PT_settings_network(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel"
|
bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel"
|
||||||
@ -156,9 +188,12 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
|||||||
return not operators.client \
|
return not operators.client \
|
||||||
or (operators.client and operators.client.state['STATE'] == 0)
|
or (operators.client and operators.client.state['STATE'] == 0)
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
self.layout.label(text="", icon='URL')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
runtime_settings = context.window_manager.session
|
runtime_settings = context.window_manager.session
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
@ -168,33 +203,40 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
|||||||
row = layout.row()
|
row = layout.row()
|
||||||
|
|
||||||
box = row.box()
|
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="")
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Timeout (ms):")
|
|
||||||
row.prop(settings, "connection_timeout", text="")
|
|
||||||
|
|
||||||
if runtime_settings.session_mode == 'HOST':
|
if runtime_settings.session_mode == 'HOST':
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.label(text="Start empty:")
|
row.label(text="Port:")
|
||||||
row.prop(settings, "start_empty", text="")
|
row.prop(settings, "port", text="")
|
||||||
|
row = box.row()
|
||||||
|
row.label(text="Start from:")
|
||||||
|
row.prop(settings, "init_method", text="")
|
||||||
|
row = box.row()
|
||||||
|
row.label(text="Admin password:")
|
||||||
|
row.prop(runtime_settings, "password", text="")
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.operator("session.start", text="HOST").host = True
|
row.operator("session.start", text="HOST").host = True
|
||||||
else:
|
else:
|
||||||
row = box.row()
|
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.prop(runtime_settings, "admin", text='Connect as admin', icon='DISCLOSURE_TRI_DOWN' if runtime_settings.admin
|
||||||
|
else 'DISCLOSURE_TRI_RIGHT')
|
||||||
|
if runtime_settings.admin:
|
||||||
|
row = box.row()
|
||||||
|
row.label(text="Password:")
|
||||||
|
row.prop(runtime_settings, "password", text="")
|
||||||
|
row = box.row()
|
||||||
row.operator("session.start", text="CONNECT").host = False
|
row.operator("session.start", text="CONNECT").host = False
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_settings_user(bpy.types.Panel):
|
class SESSION_PT_settings_user(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_SETTINGS_USER_PT_panel"
|
bl_idname = "MULTIUSER_SETTINGS_USER_PT_panel"
|
||||||
bl_label = "User"
|
bl_label = "User info"
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||||
@ -203,13 +245,16 @@ class SESSION_PT_settings_user(bpy.types.Panel):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return not operators.client \
|
return not operators.client \
|
||||||
or (operators.client and operators.client.state['STATE'] == 0)
|
or (operators.client and operators.client.state['STATE'] == 0)
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
self.layout.label(text="", icon='USER')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
runtime_settings = context.window_manager.session
|
runtime_settings = context.window_manager.session
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
# USER SETTINGS
|
# USER SETTINGS
|
||||||
row.prop(settings, "username", text="name")
|
row.prop(settings, "username", text="name")
|
||||||
@ -219,7 +264,7 @@ class SESSION_PT_settings_user(bpy.types.Panel):
|
|||||||
row = layout.row()
|
row = layout.row()
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_settings_replication(bpy.types.Panel):
|
class SESSION_PT_advanced_settings(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_SETTINGS_REPLICATION_PT_panel"
|
bl_idname = "MULTIUSER_SETTINGS_REPLICATION_PT_panel"
|
||||||
bl_label = "Advanced"
|
bl_label = "Advanced"
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
@ -232,26 +277,36 @@ class SESSION_PT_settings_replication(bpy.types.Panel):
|
|||||||
return not operators.client \
|
return not operators.client \
|
||||||
or (operators.client and operators.client.state['STATE'] == 0)
|
or (operators.client and operators.client.state['STATE'] == 0)
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
self.layout.label(text="", icon='PREFERENCES')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
runtime_settings = context.window_manager.session
|
runtime_settings = context.window_manager.session
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
# Right managment
|
|
||||||
|
net_section = layout.row().box()
|
||||||
|
net_section.label(text="Network ", icon='TRIA_DOWN')
|
||||||
|
net_section_row = net_section.row()
|
||||||
|
net_section_row.label(text="IPC Port:")
|
||||||
|
net_section_row.prop(settings, "ipc_port", text="")
|
||||||
|
net_section_row = net_section.row()
|
||||||
|
net_section_row.label(text="Timeout (ms):")
|
||||||
|
net_section_row.prop(settings, "connection_timeout", text="")
|
||||||
|
|
||||||
|
replication_section = layout.row().box()
|
||||||
|
replication_section.label(text="Replication ", icon='TRIA_DOWN')
|
||||||
|
replication_section_row = replication_section.row()
|
||||||
if runtime_settings.session_mode == 'HOST':
|
if runtime_settings.session_mode == 'HOST':
|
||||||
row = layout.row()
|
replication_section_row.prop(settings.sync_flags, "sync_render_settings")
|
||||||
row.prop(settings.sync_flags,"sync_render_settings")
|
|
||||||
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.label(text="Right strategy:")
|
|
||||||
row.prop(settings,"right_strategy",text="")
|
|
||||||
|
|
||||||
row = layout.row()
|
replication_section_row = replication_section.row()
|
||||||
|
replication_section_row.label(text="Per data type timers:")
|
||||||
row = layout.row()
|
replication_section_row = replication_section.row()
|
||||||
# Replication frequencies
|
# Replication frequencies
|
||||||
flow = row .grid_flow(
|
flow = replication_section_row .grid_flow(
|
||||||
row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
|
row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
|
||||||
line = flow.row(align=True)
|
line = flow.row(align=True)
|
||||||
line.label(text=" ")
|
line.label(text=" ")
|
||||||
@ -276,45 +331,52 @@ class SESSION_PT_user(bpy.types.Panel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return operators.client and operators.client.state['STATE'] == 2
|
return operators.client and operators.client.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
self.layout.label(text="", icon='USER')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
online_users = context.window_manager.online_users
|
online_users = context.window_manager.online_users
|
||||||
selected_user = context.window_manager.user_index
|
selected_user = context.window_manager.user_index
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
active_user = online_users[selected_user] if len(online_users)-1>=selected_user else 0
|
active_user = online_users[selected_user] if len(
|
||||||
|
online_users)-1 >= selected_user else 0
|
||||||
runtime_settings = context.window_manager.session
|
runtime_settings = context.window_manager.session
|
||||||
|
|
||||||
# Create a simple row.
|
# Create a simple row.
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
box = row.box()
|
box = row.box()
|
||||||
split = box.split(factor=0.3)
|
split = box.split(factor=0.35)
|
||||||
split.label(text="user")
|
split.label(text="user")
|
||||||
split = split.split(factor=0.5)
|
split = split.split(factor=0.5)
|
||||||
split.label(text="localisation")
|
split.label(text="location")
|
||||||
split.label(text="frame")
|
split.label(text="frame")
|
||||||
split.label(text="ping")
|
split.label(text="ping")
|
||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
layout.template_list("SESSION_UL_users", "", context.window_manager, "online_users", context.window_manager, "user_index")
|
layout.template_list("SESSION_UL_users", "", context.window_manager,
|
||||||
|
"online_users", context.window_manager, "user_index")
|
||||||
|
|
||||||
if active_user != 0 and active_user.username != settings.username:
|
if active_user != 0 and active_user.username != settings.username:
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
user_operations = row.split()
|
user_operations = row.split()
|
||||||
user_operations.alert = context.window_manager.session.time_snap_running
|
if operators.client.state['STATE'] == STATE_ACTIVE:
|
||||||
user_operations.operator(
|
|
||||||
"session.snapview",
|
user_operations.alert = context.window_manager.session.time_snap_running
|
||||||
text="",
|
user_operations.operator(
|
||||||
icon='VIEW_CAMERA').target_client = active_user.username
|
"session.snapview",
|
||||||
|
text="",
|
||||||
user_operations.alert = context.window_manager.session.user_snap_running
|
icon='VIEW_CAMERA').target_client = active_user.username
|
||||||
user_operations.operator(
|
|
||||||
"session.snaptime",
|
|
||||||
text="",
|
|
||||||
icon='TIME').target_client = active_user.username
|
|
||||||
|
|
||||||
if runtime_settings.session_mode == 'HOST':
|
user_operations.alert = context.window_manager.session.user_snap_running
|
||||||
|
user_operations.operator(
|
||||||
|
"session.snaptime",
|
||||||
|
text="",
|
||||||
|
icon='TIME').target_client = active_user.username
|
||||||
|
|
||||||
|
if operators.client.online_users[settings.username]['admin']:
|
||||||
user_operations.operator(
|
user_operations.operator(
|
||||||
"session.kick",
|
"session.kick",
|
||||||
text="",
|
text="",
|
||||||
@ -329,6 +391,7 @@ class SESSION_UL_users(bpy.types.UIList):
|
|||||||
ping = '-'
|
ping = '-'
|
||||||
frame_current = '-'
|
frame_current = '-'
|
||||||
scene_current = '-'
|
scene_current = '-'
|
||||||
|
status_icon = 'BLANK1'
|
||||||
if session:
|
if session:
|
||||||
user = session.online_users.get(item.username)
|
user = session.online_users.get(item.username)
|
||||||
if user:
|
if user:
|
||||||
@ -337,8 +400,10 @@ class SESSION_UL_users(bpy.types.UIList):
|
|||||||
if metadata and 'frame_current' in metadata:
|
if metadata and 'frame_current' in metadata:
|
||||||
frame_current = str(metadata['frame_current'])
|
frame_current = str(metadata['frame_current'])
|
||||||
scene_current = metadata['scene_current']
|
scene_current = metadata['scene_current']
|
||||||
split = layout.split(factor=0.3)
|
if user['admin']:
|
||||||
split.label(text=item.username)
|
status_icon = 'FAKE_USER_ON'
|
||||||
|
split = layout.split(factor=0.35)
|
||||||
|
split.label(text=item.username, icon=status_icon)
|
||||||
split = split.split(factor=0.5)
|
split = split.split(factor=0.5)
|
||||||
split.label(text=scene_current)
|
split.label(text=scene_current)
|
||||||
split.label(text=frame_current)
|
split.label(text=frame_current)
|
||||||
@ -359,7 +424,8 @@ class SESSION_PT_presence(bpy.types.Panel):
|
|||||||
or (operators.client and operators.client.state['STATE'] in [STATE_INITIAL, STATE_ACTIVE])
|
or (operators.client and operators.client.state['STATE'] in [STATE_INITIAL, STATE_ACTIVE])
|
||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
self.layout.prop(context.window_manager.session, "enable_presence", text="")
|
self.layout.prop(context.window_manager.session,
|
||||||
|
"enable_presence", text="",icon='OVERLAY')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
@ -367,12 +433,12 @@ class SESSION_PT_presence(bpy.types.Panel):
|
|||||||
settings = context.window_manager.session
|
settings = context.window_manager.session
|
||||||
layout.active = settings.enable_presence
|
layout.active = settings.enable_presence
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.prop(settings,"presence_show_selected")
|
col.prop(settings, "presence_show_selected")
|
||||||
col.prop(settings,"presence_show_user")
|
col.prop(settings, "presence_show_user")
|
||||||
row = layout.column()
|
row = layout.column()
|
||||||
row.active = settings.presence_show_user
|
row.active = settings.presence_show_user
|
||||||
row.prop(settings,"presence_show_far_user")
|
row.prop(settings, "presence_show_far_user")
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_services(bpy.types.Panel):
|
class SESSION_PT_services(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_SERVICE_PT_panel"
|
bl_idname = "MULTIUSER_SERVICE_PT_panel"
|
||||||
@ -386,19 +452,21 @@ class SESSION_PT_services(bpy.types.Panel):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return operators.client and operators.client.state['STATE'] == 2
|
return operators.client and operators.client.state['STATE'] == 2
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
self.layout.label(text="", icon='FILE_CACHE')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
online_users = context.window_manager.online_users
|
online_users = context.window_manager.online_users
|
||||||
selected_user = context.window_manager.user_index
|
selected_user = context.window_manager.user_index
|
||||||
settings = context.window_manager.session
|
settings = context.window_manager.session
|
||||||
active_user = online_users[selected_user] if len(online_users)-1>=selected_user else 0
|
active_user = online_users[selected_user] if len(online_users)-1 >= selected_user else 0
|
||||||
|
|
||||||
# Create a simple row.
|
# Create a simple row.
|
||||||
for name, state in operators.client.services_state.items():
|
for name, state in operators.client.services_state.items():
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.label(text=name)
|
row.label(text=name)
|
||||||
row.label(text=get_state_str(state))
|
row.label(text=get_state_str(state))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def draw_property(context, parent, property_uuid, level=0):
|
def draw_property(context, parent, property_uuid, level=0):
|
||||||
@ -425,17 +493,16 @@ def draw_property(context, parent, property_uuid, level=0):
|
|||||||
|
|
||||||
# Operations
|
# Operations
|
||||||
|
|
||||||
have_right_to_modify = runtime_settings.is_admin or \
|
have_right_to_modify = item.owner == settings.username or \
|
||||||
item.owner == settings.username or \
|
|
||||||
item.owner == RP_COMMON
|
item.owner == RP_COMMON
|
||||||
|
|
||||||
if have_right_to_modify:
|
if have_right_to_modify:
|
||||||
detail_item_box.operator(
|
detail_item_box.operator(
|
||||||
"session.commit",
|
"session.commit",
|
||||||
text="",
|
text="",
|
||||||
icon='TRIA_UP').target = item.uuid
|
icon='TRIA_UP').target = item.uuid
|
||||||
detail_item_box.separator()
|
detail_item_box.separator()
|
||||||
|
|
||||||
if item.state in [FETCHED, UP]:
|
if item.state in [FETCHED, UP]:
|
||||||
detail_item_box.operator(
|
detail_item_box.operator(
|
||||||
"session.apply",
|
"session.apply",
|
||||||
@ -464,16 +531,27 @@ def draw_property(context, parent, property_uuid, level=0):
|
|||||||
detail_item_box.label(text="", icon="DECORATE_LOCKED")
|
detail_item_box.label(text="", icon="DECORATE_LOCKED")
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_outliner(bpy.types.Panel):
|
class SESSION_PT_repository(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
|
bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
|
||||||
bl_label = "Properties"
|
bl_label = "Repository"
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return operators.client and operators.client.state['STATE'] == 2
|
session = operators.client
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
admin = False
|
||||||
|
|
||||||
|
if session and hasattr(session,'online_users'):
|
||||||
|
usr = session.online_users.get(settings.username)
|
||||||
|
if usr:
|
||||||
|
admin = usr['admin']
|
||||||
|
return hasattr(context.window_manager, 'session') and \
|
||||||
|
operators.client and \
|
||||||
|
(operators.client.state['STATE'] == STATE_ACTIVE or \
|
||||||
|
operators.client.state['STATE'] == STATE_LOBBY and admin)
|
||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
|
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
|
||||||
@ -481,10 +559,16 @@ class SESSION_PT_outliner(bpy.types.Panel):
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
if hasattr(context.window_manager, 'session'):
|
# Filters
|
||||||
# Filters
|
settings = utils.get_preferences()
|
||||||
settings = utils.get_preferences()
|
runtime_settings = context.window_manager.session
|
||||||
runtime_settings = context.window_manager.session
|
|
||||||
|
session = operators.client
|
||||||
|
usr = session.online_users.get(settings.username)
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
|
||||||
|
if session.state['STATE'] == STATE_ACTIVE:
|
||||||
flow = layout.grid_flow(
|
flow = layout.grid_flow(
|
||||||
row_major=True,
|
row_major=True,
|
||||||
columns=0,
|
columns=0,
|
||||||
@ -512,7 +596,7 @@ class SESSION_PT_outliner(bpy.types.Panel):
|
|||||||
if operators.client.get(uuid=key).str_type
|
if operators.client.get(uuid=key).str_type
|
||||||
in types_filter]
|
in types_filter]
|
||||||
|
|
||||||
if client_keys and len(client_keys) > 0:
|
if client_keys:
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
for key in client_keys:
|
for key in client_keys:
|
||||||
draw_property(context, col, key)
|
draw_property(context, col, key)
|
||||||
@ -520,6 +604,11 @@ class SESSION_PT_outliner(bpy.types.Panel):
|
|||||||
else:
|
else:
|
||||||
row.label(text="Empty")
|
row.label(text="Empty")
|
||||||
|
|
||||||
|
elif session.state['STATE'] == STATE_LOBBY and usr and usr['admin']:
|
||||||
|
row.operator("session.init", icon='TOOL_SETTINGS', text="Init")
|
||||||
|
else:
|
||||||
|
row.label(text="Waiting to start")
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
SESSION_UL_users,
|
SESSION_UL_users,
|
||||||
@ -527,11 +616,11 @@ classes = (
|
|||||||
SESSION_PT_settings_user,
|
SESSION_PT_settings_user,
|
||||||
SESSION_PT_settings_network,
|
SESSION_PT_settings_network,
|
||||||
SESSION_PT_presence,
|
SESSION_PT_presence,
|
||||||
SESSION_PT_settings_replication,
|
SESSION_PT_advanced_settings,
|
||||||
SESSION_PT_user,
|
SESSION_PT_user,
|
||||||
SESSION_PT_services,
|
SESSION_PT_services,
|
||||||
SESSION_PT_outliner,
|
SESSION_PT_repository,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ def main():
|
|||||||
if len(sys.argv) > 2:
|
if len(sys.argv) > 2:
|
||||||
blender_rev = sys.argv[2]
|
blender_rev = sys.argv[2]
|
||||||
else:
|
else:
|
||||||
blender_rev = "2.82"
|
blender_rev = "2.90.0"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev)
|
exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev)
|
||||||
|
@ -30,9 +30,11 @@ CONSTRAINTS_TYPES = [
|
|||||||
'COPY_ROTATION', 'COPY_SCALE', 'COPY_TRANSFORMS', 'LIMIT_DISTANCE',
|
'COPY_ROTATION', 'COPY_SCALE', 'COPY_TRANSFORMS', 'LIMIT_DISTANCE',
|
||||||
'LIMIT_LOCATION', 'LIMIT_ROTATION', 'LIMIT_SCALE', 'MAINTAIN_VOLUME',
|
'LIMIT_LOCATION', 'LIMIT_ROTATION', 'LIMIT_SCALE', 'MAINTAIN_VOLUME',
|
||||||
'TRANSFORM', 'TRANSFORM_CACHE', 'CLAMP_TO', 'DAMPED_TRACK', 'IK',
|
'TRANSFORM', 'TRANSFORM_CACHE', 'CLAMP_TO', 'DAMPED_TRACK', 'IK',
|
||||||
'LOCKED_TRACK', 'SPLINE_IK', 'STRETCH_TO', 'TRACK_TO', 'ACTION',
|
'LOCKED_TRACK', 'STRETCH_TO', 'TRACK_TO', 'ACTION',
|
||||||
'ARMATURE', 'CHILD_OF', 'FLOOR', 'FOLLOW_PATH', 'PIVOT', 'SHRINKWRAP']
|
'ARMATURE', 'CHILD_OF', 'FLOOR', 'FOLLOW_PATH', 'PIVOT', 'SHRINKWRAP']
|
||||||
|
|
||||||
|
#temporary disabled 'SPLINE_IK' until its fixed
|
||||||
|
|
||||||
def test_object(clear_blend):
|
def test_object(clear_blend):
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
bpy.ops.mesh.primitive_cube_add(
|
||||||
enter_editmode=False, align='WORLD', location=(0, 0, 0))
|
enter_editmode=False, align='WORLD', location=(0, 0, 0))
|
||||||
|