Merge branch 'develop' into feature/event_driven_updates
@ -5,4 +5,3 @@ stages:
|
||||
include:
|
||||
- local: .gitlab/ci/test.gitlab-ci.yml
|
||||
- local: .gitlab/ci/build.gitlab-ci.yml
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
build:
|
||||
stage: build
|
||||
image: python:latest
|
||||
image: debian:stable-slim
|
||||
script:
|
||||
- git submodule init
|
||||
- git submodule update
|
||||
- cd multi_user/libs/replication
|
||||
- rm -rf tests .git .gitignore script
|
||||
|
||||
artifacts:
|
||||
@ -12,3 +9,4 @@ build:
|
||||
paths:
|
||||
- multi_user
|
||||
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
test:
|
||||
stage: test
|
||||
image: python:latest
|
||||
image: python:3.7
|
||||
script:
|
||||
- git submodule init
|
||||
- git submodule update
|
||||
- apt update
|
||||
- apt-get update
|
||||
# install blender to get all required dependencies
|
||||
# TODO: indtall only dependencies
|
||||
- apt install -f -y blender
|
||||
- pip install blender-addon-tester
|
||||
- python -m pip install blender-addon-tester
|
||||
- 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)
|
||||
|
||||
|
||||
/label ~bug
|
||||
/label ~type::bug
|
||||
/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
|
||||
|
||||
- Auto updater support
|
||||
- Performances improvements on Meshes, Gpencils, Actions
|
||||
- Big Performances improvements on Meshes, Gpencils, Actions
|
||||
- Multi-scene workflow support
|
||||
- Render setting synchronisation
|
||||
- Render setting synchronization
|
||||
- Kick command
|
||||
- Dedicated server with a basic command set
|
||||
- Administrator session status
|
||||
- Tests
|
||||
- Blender 2.83-2.90 support
|
||||
|
||||
### Changed
|
||||
|
||||
- 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
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
.. 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 ?
|
||||
|
||||
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
|
||||
===============
|
||||
|
||||
@ -7,3 +8,4 @@ Getting started
|
||||
|
||||
install
|
||||
quickstart
|
||||
glossary
|
||||
|
@ -2,8 +2,12 @@
|
||||
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.
|
||||
2. Run blender as administrator (to allow python dependencies auto-installation).
|
||||
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
|
||||
===========
|
||||
|
||||
*All settings are located under: `View3D -> Sidebar -> Multiuser panel`*
|
||||
.. hint::
|
||||
*All session related settings are located under: `View3D -> Sidebar -> Multiuser panel`*
|
||||
|
||||
Session setup
|
||||
=============
|
||||
This section describe how to create or join a collaborative session.
|
||||
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:
|
||||
|
||||
---------------------
|
||||
1. User information's
|
||||
---------------------
|
||||
- :ref:`how-to-host`
|
||||
- :ref:`how-to-join`
|
||||
- :ref:`how-to-manage`
|
||||
|
||||
.. image:: img/quickstart_user_infos.png
|
||||
.. _how-to-host:
|
||||
|
||||
- **name**: username.
|
||||
- **color**: color used to represent the user into other user workspace.
|
||||
How to host a session
|
||||
=====================
|
||||
|
||||
----------
|
||||
2. Network
|
||||
----------
|
||||
The multi-user add-on rely on a Client-Server architecture.
|
||||
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 |
|
||||
+-----------------------------------+-------------------------------------+
|
||||
-----------------------------
|
||||
1. Fill your user information
|
||||
-----------------------------
|
||||
|
||||
**Port configuration:**
|
||||
For now, a session use 4 ports to run.
|
||||
If 5555 is given in host settings, it will use 5555, 5556 (5555+1), 5557 (5555+2), 5558 (5555+3).
|
||||
The **User Info** panel (See image below) allow you to constomize your online identity.
|
||||
|
||||
------------
|
||||
2.1 Advanced
|
||||
------------
|
||||
.. figure:: img/quickstart_user_info.png
|
||||
:align: center
|
||||
|
||||
.. image:: img/quickstart_advanced.png
|
||||
User info panel
|
||||
|
||||
**Synchronise render settings** (only host) enable replication of EEVEE and CYCLES render settings to match render between clients.
|
||||
|
||||
**Right strategy** (only host) enable you to choose between a strict and a relaxed pattern:
|
||||
Let's fill those tow field:
|
||||
|
||||
- **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.
|
||||
- **name**: your online name.
|
||||
- **color**: a color used to represent you into other user workspace(see image below).
|
||||
|
||||
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:
|
||||
During online sessions, other users will see your selected object and camera hilghlited in your profile color.
|
||||
|
||||
- **Refresh**: pushed data update rate (in second)
|
||||
- **Apply**: pulled data update rate (in second)
|
||||
.. _user-representation:
|
||||
|
||||
.. note:: Per-data type settings will soon be revamped for simplification purposes
|
||||
.. figure:: img/quickstart_user_representation.png
|
||||
:align: center
|
||||
|
||||
Session Management
|
||||
==================
|
||||
User viewport representation
|
||||
|
||||
This section describe tools available during a collaborative session.
|
||||
--------------------
|
||||
2. Setup the network
|
||||
--------------------
|
||||
|
||||
---------------
|
||||
Connected users
|
||||
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.
|
||||
By selecting a user in the list you'll have access to different **actions**:
|
||||
.. figure:: img/quickstart_snap_time.gif
|
||||
:align: center
|
||||
|
||||
- The **camera button** allow you to snap on the user viewpoint.
|
||||
- The **time button** allow you to snap on the user time.
|
||||
- The **cross button** [**host only**] allow the admin to kick users
|
||||
Snap time in action
|
||||
|
||||
-------------------
|
||||
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 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.
|
||||
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.
|
||||
- The server store the "master" version of the work.
|
||||
- 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 |
|
||||
@ -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 |
|
||||
+---------------------------------------+-------------------+------------------------------------------------------------------------------------+
|
||||
|
||||
.. _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
|
||||
- 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
|
||||
======
|
||||
@ -43,6 +48,7 @@ Documentation is organized into the following sections:
|
||||
|
||||
getting_started/install
|
||||
getting_started/quickstart
|
||||
getting_started/glossary
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
@ -1,47 +1,278 @@
|
||||
================
|
||||
Advanced hosting
|
||||
================
|
||||
.. _internet-guide:
|
||||
|
||||
This tutorial aims to guide you to host a collaborative Session on internet.
|
||||
|
||||
.. note::
|
||||
This tutorial will change soon with the new dedicated server.
|
||||
|
||||
|
||||
The multi-user network architecture is based on a clients-server model. The communication protocol use four ports to communicate with client:
|
||||
|
||||
* Commands: command transmission (such as **snapshots**, **change_rights**, etc.)
|
||||
* Subscriber: pull data
|
||||
* Publisher: push data
|
||||
* TTL (time to leave): used to ping each clients
|
||||
===================
|
||||
Hosting on internet
|
||||
===================
|
||||
|
||||
.. 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>`_).
|
||||
|
||||
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.
|
||||
|
||||
.. image:: img/hosting_guide_port.png
|
||||
.. figure:: img/hosting_guide_port.png
|
||||
:align: center
|
||||
:alt: Port
|
||||
:width: 200px
|
||||
|
||||
Port in host settings
|
||||
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)
|
||||
* Publisher: 5557 (**5555** +2)
|
||||
* TTL: 5558 (**5555** +3)
|
||||
|
||||
Now that we know which port are needed to communicate we need to allow other computer to communicate with our one.
|
||||
By default your router shall block those ports. In order grant server access to people from internet you have multiple options:
|
||||
|
||||
1. Simple: use a third party software like `HAMACHI <https://vpn.net/>`_ (Free until 5 users) or `ZEROTIER <https://www.zerotier.com/download/>`_ to handle network sharing.
|
||||
|
||||
2. Harder: Setup a VPN server and allow distant user to connect to your VPN.
|
||||
|
||||
3. **Not secure** but simple: Setup port forwarding for each ports (for example 5555,5556,5557 and 5558 in our case). You can follow this `guide <https://www.wikihow.com/Set-Up-Port-Forwarding-on-a-Router>`_ for example.
|
||||
|
||||
Once you have setup the network, you can run **HOST** in order to start the server. Then other users could join your session in the regular way.
|
||||
|
||||
|
||||
|
||||
|
||||
Those four ports need to be accessible from the client otherwise it won't work at all !
|
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",
|
||||
"version": (0, 0, 3),
|
||||
"description": "Enable real-time collaborative workflow inside blender",
|
||||
"blender": (2, 80, 0),
|
||||
"blender": (2, 82, 0),
|
||||
"location": "3D View > Sidebar > Multi-User tab",
|
||||
"warning": "Unstable addon, use it at your own risks",
|
||||
"category": "Collaboration",
|
||||
@ -45,21 +45,15 @@ from . import environment, utils
|
||||
|
||||
# TODO: remove dependency as soon as replication will be installed as a module
|
||||
DEPENDENCIES = {
|
||||
("zmq","zmq"),
|
||||
("jsondiff","jsondiff"),
|
||||
("deepdiff", "deepdiff")
|
||||
("replication", '0.0.20'),
|
||||
("deepdiff", '5.0.1'),
|
||||
}
|
||||
|
||||
|
||||
libs = os.path.dirname(os.path.abspath(__file__))+"\\libs\\replication\\replication"
|
||||
|
||||
def register():
|
||||
# Setup logging policy
|
||||
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
|
||||
|
||||
if libs not in sys.path:
|
||||
sys.path.append(libs)
|
||||
|
||||
try:
|
||||
environment.setup(DEPENDENCIES, bpy.app.binary_path_python)
|
||||
except ModuleNotFoundError:
|
||||
@ -80,7 +74,9 @@ def register():
|
||||
|
||||
bpy.types.WindowManager.session = bpy.props.PointerProperty(
|
||||
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(
|
||||
type=preferences.SessionUser
|
||||
)
|
||||
|
@ -722,19 +722,20 @@ class Singleton_updater(object):
|
||||
|
||||
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:
|
||||
request = urllib.request.Request(url)
|
||||
context = ssl._create_unverified_context()
|
||||
import urllib3
|
||||
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
|
||||
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
|
||||
r.release_conn()
|
||||
if self._verbose: print("Successfully downloaded update zip")
|
||||
return True
|
||||
except Exception as e:
|
||||
|
@ -38,7 +38,7 @@ __all__ = [
|
||||
] # Order here defines execution order
|
||||
|
||||
from . import *
|
||||
from ..libs.replication.replication.data import ReplicatedDataFactory
|
||||
from replication.data import ReplicatedDataFactory
|
||||
|
||||
def types_to_register():
|
||||
return __all__
|
||||
|
@ -92,6 +92,7 @@ class BlArmature(BlDatablock):
|
||||
new_bone.head = bone_data['head_local']
|
||||
new_bone.tail_radius = bone_data['tail_radius']
|
||||
new_bone.head_radius = bone_data['head_radius']
|
||||
# new_bone.roll = bone_data['roll']
|
||||
|
||||
if 'parent' in bone_data:
|
||||
new_bone.parent = target.edit_bones[data['bones']
|
||||
@ -123,7 +124,8 @@ class BlArmature(BlDatablock):
|
||||
'use_connect',
|
||||
'parent',
|
||||
'name',
|
||||
'layers'
|
||||
'layers',
|
||||
# 'roll',
|
||||
|
||||
]
|
||||
data = dumper.dump(instance)
|
||||
|
@ -45,13 +45,22 @@ class BlCamera(BlDatablock):
|
||||
if dof_settings:
|
||||
loader.load(target.dof, dof_settings)
|
||||
|
||||
background_images = data.get('background_images')
|
||||
|
||||
if background_images:
|
||||
target.background_images.clear()
|
||||
for img_name, img_data in background_images.items():
|
||||
target_img = target.background_images.new()
|
||||
target_img.image = bpy.data.images[img_name]
|
||||
loader.load(target_img, img_data)
|
||||
|
||||
def _dump_implementation(self, data, instance=None):
|
||||
assert(instance)
|
||||
|
||||
# TODO: background image support
|
||||
|
||||
dumper = Dumper()
|
||||
dumper.depth = 2
|
||||
dumper.depth = 3
|
||||
dumper.include_filter = [
|
||||
"name",
|
||||
'type',
|
||||
@ -79,7 +88,24 @@ class BlCamera(BlDatablock):
|
||||
'sensor_fit',
|
||||
'sensor_height',
|
||||
'sensor_width',
|
||||
'show_background_images',
|
||||
'background_images',
|
||||
'alpha',
|
||||
'display_depth',
|
||||
'frame_method',
|
||||
'offset',
|
||||
'rotation',
|
||||
'scale',
|
||||
'use_flip_x',
|
||||
'use_flip_y',
|
||||
'image'
|
||||
]
|
||||
return dumper.dump(instance)
|
||||
|
||||
def _resolve_deps_implementation(self):
|
||||
deps = []
|
||||
for background in self.instance.background_images:
|
||||
if background.image:
|
||||
deps.append(background.image)
|
||||
|
||||
return deps
|
||||
|
@ -21,8 +21,8 @@ import mathutils
|
||||
|
||||
from .. import utils
|
||||
from .dump_anything import Loader, Dumper
|
||||
from ..libs.replication.replication.data import ReplicatedDatablock
|
||||
from ..libs.replication.replication.constants import (UP, DIFF_BINARY)
|
||||
from replication.data import ReplicatedDatablock
|
||||
from replication.constants import (UP, DIFF_BINARY)
|
||||
|
||||
|
||||
def has_action(target):
|
||||
@ -107,24 +107,25 @@ class BlDatablock(ReplicatedDatablock):
|
||||
(self.data and 'library' in self.data)
|
||||
|
||||
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_root = getattr(bpy.data, self.bl_id)
|
||||
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
|
||||
|
||||
# In case of lost uuid (ex: undo), resolve by name and reassign it
|
||||
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:
|
||||
setattr(datablock_ref, 'uuid', self.uuid)
|
||||
|
||||
return datablock_ref
|
||||
self.instance = datablock_ref
|
||||
|
||||
def _dump(self, instance=None):
|
||||
dumper = Dumper()
|
||||
|
@ -35,7 +35,6 @@ def dump_image(image):
|
||||
os.makedirs(prefs.cache_directory, exist_ok=True)
|
||||
image.file_format = "PNG"
|
||||
image.save()
|
||||
logging.info( image.filepath_raw )
|
||||
|
||||
if image.source == "FILE":
|
||||
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 .bl_datablock import BlDatablock
|
||||
from ..libs.replication.replication.exception import ContextError
|
||||
from replication.exception import ContextError
|
||||
|
||||
POINT = ['co', 'weight_softbody', 'co_deform']
|
||||
|
||||
|
@ -19,11 +19,13 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .. import utils
|
||||
from .dump_anything import Loader, Dumper
|
||||
from .bl_datablock import BlDatablock
|
||||
|
||||
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
||||
|
||||
def load_node(node_data, node_tree):
|
||||
""" Load a node into a node_tree from a dict
|
||||
@ -38,14 +40,13 @@ def load_node(node_data, node_tree):
|
||||
|
||||
loader.load(target_node, node_data)
|
||||
|
||||
|
||||
|
||||
for input in node_data["inputs"]:
|
||||
if hasattr(target_node.inputs[input], "default_value"):
|
||||
try:
|
||||
target_node.inputs[input].default_value = node_data["inputs"][input]["default_value"]
|
||||
except:
|
||||
logging.error(f"Material {input} parameter not supported, skipping")
|
||||
logging.error(
|
||||
f"Material {input} parameter not supported, skipping")
|
||||
|
||||
|
||||
def load_links(links_data, node_tree):
|
||||
@ -60,7 +61,6 @@ def load_links(links_data, node_tree):
|
||||
for link in links_data:
|
||||
input_socket = node_tree.nodes[link['to_node']].inputs[int(link['to_socket'])]
|
||||
output_socket = node_tree.nodes[link['from_node']].outputs[int(link['from_socket'])]
|
||||
|
||||
node_tree.links.new(input_socket, output_socket)
|
||||
|
||||
|
||||
@ -75,11 +75,13 @@ def dump_links(links):
|
||||
links_data = []
|
||||
|
||||
for link in links:
|
||||
to_socket = NODE_SOCKET_INDEX.search(link.to_socket.path_from_id()).group(1)
|
||||
from_socket = NODE_SOCKET_INDEX.search(link.from_socket.path_from_id()).group(1)
|
||||
links_data.append({
|
||||
'to_node':link.to_node.name,
|
||||
'to_socket':link.to_socket.path_from_id()[-2:-1],
|
||||
'from_node':link.from_node.name,
|
||||
'from_socket':link.from_socket.path_from_id()[-2:-1],
|
||||
'to_node': link.to_node.name,
|
||||
'to_socket': to_socket,
|
||||
'from_node': link.from_node.name,
|
||||
'from_socket': from_socket,
|
||||
})
|
||||
|
||||
return links_data
|
||||
@ -176,14 +178,13 @@ class BlMaterial(BlDatablock):
|
||||
loader.load(
|
||||
target.grease_pencil, data['grease_pencil'])
|
||||
|
||||
|
||||
if data["use_nodes"]:
|
||||
if target.node_tree is None:
|
||||
target.use_nodes = True
|
||||
|
||||
target.node_tree.nodes.clear()
|
||||
|
||||
loader.load(target,data)
|
||||
loader.load(target, data)
|
||||
|
||||
# Load nodes
|
||||
for node in data["node_tree"]["nodes"]:
|
||||
@ -265,4 +266,3 @@ class BlMaterial(BlDatablock):
|
||||
deps.append(self.instance.library)
|
||||
|
||||
return deps
|
||||
|
||||
|
@ -23,8 +23,8 @@ import logging
|
||||
import numpy as np
|
||||
|
||||
from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection
|
||||
from ..libs.replication.replication.constants import DIFF_BINARY
|
||||
from ..libs.replication.replication.exception import ContextError
|
||||
from replication.constants import DIFF_BINARY
|
||||
from replication.exception import ContextError
|
||||
from .bl_datablock import BlDatablock
|
||||
|
||||
|
||||
@ -61,7 +61,9 @@ class BlMesh(BlDatablock):
|
||||
return instance
|
||||
|
||||
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.load(target, data)
|
||||
|
||||
|
@ -22,7 +22,7 @@ import logging
|
||||
|
||||
from .dump_anything import Loader, Dumper
|
||||
from .bl_datablock import BlDatablock
|
||||
from ..libs.replication.replication.exception import ContextError
|
||||
from replication.exception import ContextError
|
||||
|
||||
|
||||
def load_pose(target_bone, data):
|
||||
@ -87,38 +87,7 @@ class BlObject(BlDatablock):
|
||||
return instance
|
||||
|
||||
def _load_implementation(self, data, target):
|
||||
# Load transformation data
|
||||
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
|
||||
if 'vertex_groups' in data:
|
||||
@ -152,6 +121,45 @@ class BlObject(BlDatablock):
|
||||
|
||||
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):
|
||||
assert(instance)
|
||||
|
||||
@ -171,10 +179,21 @@ class BlObject(BlDatablock):
|
||||
"library",
|
||||
"empty_display_type",
|
||||
"empty_display_size",
|
||||
"empty_image_offset",
|
||||
"empty_image_depth",
|
||||
"empty_image_side",
|
||||
"show_empty_image_orthographic",
|
||||
"show_empty_image_perspective",
|
||||
"show_empty_image_only_axis_aligned",
|
||||
"use_empty_image_alpha",
|
||||
"color"
|
||||
"instance_collection",
|
||||
"instance_type",
|
||||
"location",
|
||||
"scale",
|
||||
'lock_location',
|
||||
'lock_rotation',
|
||||
'lock_scale',
|
||||
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
|
||||
]
|
||||
|
||||
@ -186,7 +205,7 @@ class BlObject(BlDatablock):
|
||||
# MODIFIERS
|
||||
if hasattr(instance, 'modifiers'):
|
||||
dumper.include_filter = None
|
||||
dumper.depth = 2
|
||||
dumper.depth = 1
|
||||
data["modifiers"] = {}
|
||||
for index, modifier in enumerate(instance.modifiers):
|
||||
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_collection = (
|
||||
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_matrix = (self._dump_matrix_as_leaf,
|
||||
self._dump_matrix_as_leaf)
|
||||
|
@ -20,7 +20,14 @@ import logging
|
||||
import bpy
|
||||
|
||||
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():
|
||||
@ -107,71 +114,66 @@ class DynamicRightSelectTimer(Timer):
|
||||
if self._user is None:
|
||||
self._user = session.online_users.get(settings.username)
|
||||
|
||||
if self._right_strategy is None:
|
||||
self._right_strategy = session.config[
|
||||
'right_strategy']
|
||||
|
||||
if self._user:
|
||||
current_selection = utils.get_selected_objects(
|
||||
bpy.context.scene,
|
||||
bpy.data.window_managers['WinMan'].windows[0].view_layer
|
||||
)
|
||||
if current_selection != self._last_selection:
|
||||
if self._right_strategy == RP_COMMON:
|
||||
obj_common = [
|
||||
o for o in self._last_selection if o not in current_selection]
|
||||
obj_ours = [
|
||||
o for o in current_selection if o not in self._last_selection]
|
||||
obj_common = [
|
||||
o for o in self._last_selection if o not in current_selection]
|
||||
obj_ours = [
|
||||
o for o in current_selection if o not in self._last_selection]
|
||||
|
||||
# change old selection right to common
|
||||
for obj in obj_common:
|
||||
node = session.get(uuid=obj)
|
||||
# change old selection right to common
|
||||
for obj in obj_common:
|
||||
node = session.get(uuid=obj)
|
||||
|
||||
if node and (node.owner == settings.username or node.owner == RP_COMMON):
|
||||
recursive = True
|
||||
if node.data and 'instance_type' in node.data.keys():
|
||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
RP_COMMON,
|
||||
recursive=recursive)
|
||||
if node and (node.owner == settings.username or node.owner == RP_COMMON):
|
||||
recursive = True
|
||||
if node.data and 'instance_type' in node.data.keys():
|
||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
RP_COMMON,
|
||||
recursive=recursive)
|
||||
|
||||
# change new selection to our
|
||||
for obj in obj_ours:
|
||||
node = session.get(uuid=obj)
|
||||
# change new selection to our
|
||||
for obj in obj_ours:
|
||||
node = session.get(uuid=obj)
|
||||
|
||||
if node and node.owner == RP_COMMON:
|
||||
recursive = True
|
||||
if node.data and 'instance_type' in node.data.keys():
|
||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||
if node and node.owner == RP_COMMON:
|
||||
recursive = True
|
||||
if node.data and 'instance_type' in node.data.keys():
|
||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
settings.username,
|
||||
recursive=recursive)
|
||||
else:
|
||||
return
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
settings.username,
|
||||
recursive=recursive)
|
||||
else:
|
||||
return
|
||||
|
||||
self._last_selection = current_selection
|
||||
self._last_selection = current_selection
|
||||
|
||||
user_metadata = {
|
||||
'selected_objects': current_selection
|
||||
}
|
||||
user_metadata = {
|
||||
'selected_objects': current_selection
|
||||
}
|
||||
|
||||
session.update_user_metadata(user_metadata)
|
||||
logging.debug("Update selection")
|
||||
session.update_user_metadata(user_metadata)
|
||||
logging.debug("Update selection")
|
||||
|
||||
# Fix deselection until right managment refactoring (with Roles concepts)
|
||||
if len(current_selection) == 0 and self._right_strategy == RP_COMMON:
|
||||
owned_keys = session.list(
|
||||
filter_owner=settings.username)
|
||||
for key in owned_keys:
|
||||
node = session.get(uuid=key)
|
||||
# Fix deselection until right managment refactoring (with Roles concepts)
|
||||
if len(current_selection) == 0 and self._right_strategy == RP_COMMON:
|
||||
owned_keys = session.list(
|
||||
filter_owner=settings.username)
|
||||
for key in owned_keys:
|
||||
node = session.get(uuid=key)
|
||||
|
||||
session.change_owner(
|
||||
key,
|
||||
RP_COMMON,
|
||||
recursive=recursive)
|
||||
session.change_owner(
|
||||
key,
|
||||
RP_COMMON,
|
||||
recursive=recursive)
|
||||
|
||||
for user, user_info in session.online_users.items():
|
||||
if user != settings.username:
|
||||
@ -236,7 +238,7 @@ class DrawClient(Draw):
|
||||
|
||||
|
||||
class ClientUpdate(Timer):
|
||||
def __init__(self, timout=.016):
|
||||
def __init__(self, timout=.032):
|
||||
super().__init__(timout)
|
||||
self.handle_quit = False
|
||||
self.users_metadata = {}
|
||||
@ -247,11 +249,7 @@ class ClientUpdate(Timer):
|
||||
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.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]:
|
||||
local_user = operators.client.online_users.get(settings.username)
|
||||
|
||||
if not local_user:
|
||||
@ -285,7 +283,7 @@ class ClientUpdate(Timer):
|
||||
settings.client_color.g,
|
||||
settings.client_color.b,
|
||||
1),
|
||||
'frame_current':bpy.context.scene.frame_current,
|
||||
'frame_current': bpy.context.scene.frame_current,
|
||||
'scene_current': scene_current
|
||||
}
|
||||
session.update_user_metadata(metadata)
|
||||
@ -295,35 +293,40 @@ class ClientUpdate(Timer):
|
||||
elif scene_current != local_user_metadata['scene_current']:
|
||||
local_user_metadata['scene_current'] = scene_current
|
||||
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_matrix'] = presence.get_view_matrix()
|
||||
session.update_user_metadata(local_user_metadata)
|
||||
# sync online users
|
||||
session_users = operators.client.online_users
|
||||
ui_users = bpy.context.window_manager.online_users
|
||||
|
||||
for index, user in enumerate(ui_users):
|
||||
if user.username not in session_users.keys():
|
||||
ui_users.remove(index)
|
||||
renderer.flush_selection()
|
||||
renderer.flush_users()
|
||||
break
|
||||
class SessionStatusUpdate(Timer):
|
||||
def __init__(self, timout=1):
|
||||
super().__init__(timout)
|
||||
|
||||
for user in session_users:
|
||||
if user not in ui_users:
|
||||
new_key = ui_users.add()
|
||||
new_key.name = user
|
||||
new_key.username = user
|
||||
elif session.state['STATE'] == STATE_QUITTING:
|
||||
presence.refresh_sidebar_view()
|
||||
self.handle_quit = True
|
||||
elif session.state['STATE'] == STATE_INITIAL and self.handle_quit:
|
||||
self.handle_quit = False
|
||||
presence.refresh_sidebar_view()
|
||||
def execute(self):
|
||||
presence.refresh_sidebar_view()
|
||||
|
||||
operators.unregister_delayables()
|
||||
class SessionUserSync(Timer):
|
||||
def __init__(self, timout=1):
|
||||
super().__init__(timout)
|
||||
|
||||
presence.renderer.stop()
|
||||
def execute(self):
|
||||
session = getattr(operators, 'client', None)
|
||||
renderer = getattr(presence, 'renderer', None)
|
||||
|
||||
presence.refresh_sidebar_view()
|
||||
if session and renderer:
|
||||
# sync online users
|
||||
session_users = operators.client.online_users
|
||||
ui_users = bpy.context.window_manager.online_users
|
||||
|
||||
for index, user in enumerate(ui_users):
|
||||
if user.username not in session_users.keys():
|
||||
ui_users.remove(index)
|
||||
renderer.flush_selection()
|
||||
renderer.flush_users()
|
||||
break
|
||||
|
||||
for user in session_users:
|
||||
if user not in ui_users:
|
||||
new_key = ui_users.add()
|
||||
new_key.name = user
|
||||
new_key.username = user
|
@ -22,16 +22,21 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
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")
|
||||
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
|
||||
SUBPROCESS_DIR = None
|
||||
|
||||
|
||||
rtypes = []
|
||||
|
||||
|
||||
def module_can_be_imported(name):
|
||||
try:
|
||||
__import__(name)
|
||||
@ -42,19 +47,43 @@ def module_can_be_imported(name):
|
||||
|
||||
def install_pip():
|
||||
# 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), str(get_pip_path)], cwd=SUBPROCESS_DIR)
|
||||
subprocess.run([str(PYTHON_PATH), "-m", "ensurepip"])
|
||||
|
||||
|
||||
def install_package(name):
|
||||
logging.debug(f"Using {PYTHON_PATH} for installation")
|
||||
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", name])
|
||||
def install_package(name, version):
|
||||
logging.info(f"installing {name} version...")
|
||||
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):
|
||||
if not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
|
||||
|
||||
def setup(dependencies, python_path):
|
||||
global PYTHON_PATH, SUBPROCESS_DIR
|
||||
|
||||
@ -64,7 +93,9 @@ def setup(dependencies, python_path):
|
||||
if not module_can_be_imported("pip"):
|
||||
install_pip()
|
||||
|
||||
for module_name, package_name in dependencies:
|
||||
if not module_can_be_imported(module_name):
|
||||
install_package(package_name)
|
||||
for package_name, package_version in dependencies:
|
||||
if not module_can_be_imported(package_name):
|
||||
install_package(package_name, package_version)
|
||||
module_can_be_imported(package_name)
|
||||
elif not check_package_version(package_name, package_version):
|
||||
install_package(package_name, package_version)
|
||||
|
@ -26,37 +26,25 @@ import time
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, Popen, TimeoutExpired
|
||||
import zmq
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
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_SYNCING,UP)
|
||||
from .libs.replication.replication.data import ReplicatedDataFactory
|
||||
from .libs.replication.replication.exception import NonAuthorizedOperationError
|
||||
from .libs.replication.replication.interface import Session
|
||||
STATE_SYNCING)
|
||||
from replication.data import ReplicatedDataFactory
|
||||
from replication.exception import NonAuthorizedOperationError
|
||||
from replication.interface import Session
|
||||
|
||||
|
||||
client = None
|
||||
delayables = []
|
||||
ui_context = None
|
||||
stop_modal_executor = False
|
||||
modal_executor_queue = None
|
||||
server_process = None
|
||||
|
||||
def unregister_delayables():
|
||||
global delayables, stop_modal_executor
|
||||
|
||||
for d in delayables:
|
||||
try:
|
||||
d.unregister()
|
||||
except:
|
||||
continue
|
||||
|
||||
stop_modal_executor = True
|
||||
|
||||
# OPERATORS
|
||||
|
||||
@ -73,18 +61,18 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client, delayables, ui_context, server_process
|
||||
global client, delayables
|
||||
|
||||
settings = utils.get_preferences()
|
||||
runtime_settings = context.window_manager.session
|
||||
users = bpy.data.window_managers['WinMan'].online_users
|
||||
admin_pass = runtime_settings.password
|
||||
|
||||
# TODO: Sync server clients
|
||||
users.clear()
|
||||
delayables.clear()
|
||||
|
||||
bpy_factory = ReplicatedDataFactory()
|
||||
supported_bl_types = []
|
||||
ui_context = context.copy()
|
||||
|
||||
# init the factory with supported types
|
||||
for type in bl_types.types_to_register():
|
||||
@ -105,38 +93,39 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
|
||||
client = Session(
|
||||
factory=bpy_factory,
|
||||
python_path=bpy.app.binary_path_python,
|
||||
default_strategy=settings.right_strategy)
|
||||
python_path=bpy.app.binary_path_python)
|
||||
|
||||
delayables.append(delayable.ApplyTimer())
|
||||
|
||||
# Host a session
|
||||
if self.host:
|
||||
# Scene setup
|
||||
if settings.start_empty:
|
||||
if settings.init_method == 'EMPTY':
|
||||
utils.clean_scene()
|
||||
|
||||
try:
|
||||
for scene in bpy.data.scenes:
|
||||
scene_uuid = client.add(scene)
|
||||
client.commit(scene_uuid)
|
||||
runtime_settings.is_host = True
|
||||
runtime_settings.internet_ip = environment.get_ip()
|
||||
|
||||
for scene in bpy.data.scenes:
|
||||
client.add(scene)
|
||||
|
||||
try:
|
||||
client.host(
|
||||
id=settings.username,
|
||||
address=settings.ip,
|
||||
port=settings.port,
|
||||
ipc_port=settings.ipc_port,
|
||||
timeout=settings.connection_timeout
|
||||
timeout=settings.connection_timeout,
|
||||
password=admin_pass
|
||||
)
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
logging.error(f"Error: {e}")
|
||||
finally:
|
||||
runtime_settings.is_admin = True
|
||||
|
||||
# Join a session
|
||||
else:
|
||||
utils.clean_scene()
|
||||
if not runtime_settings.admin:
|
||||
utils.clean_scene()
|
||||
# regular client, no password needed
|
||||
admin_pass = None
|
||||
|
||||
try:
|
||||
client.connect(
|
||||
@ -144,13 +133,12 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
address=settings.ip,
|
||||
port=settings.port,
|
||||
ipc_port=settings.ipc_port,
|
||||
timeout=settings.connection_timeout
|
||||
timeout=settings.connection_timeout,
|
||||
password=admin_pass
|
||||
)
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
logging.error(f"Error: {e}")
|
||||
finally:
|
||||
runtime_settings.is_admin = False
|
||||
self.report({'ERROR'}, str(e))
|
||||
logging.error(str(e))
|
||||
|
||||
# Background client updates service
|
||||
#TODO: Refactoring
|
||||
@ -158,16 +146,44 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
delayables.append(delayable.DrawClient())
|
||||
delayables.append(delayable.DynamicRightSelectTimer())
|
||||
|
||||
# Launch drawing module
|
||||
if runtime_settings.enable_presence:
|
||||
presence.renderer.run()
|
||||
session_update = delayable.SessionStatusUpdate()
|
||||
session_user_sync = delayable.SessionUserSync()
|
||||
session_update.register()
|
||||
session_user_sync.register()
|
||||
|
||||
# Register blender main thread tools
|
||||
for d in delayables:
|
||||
d.register()
|
||||
delayables.append(session_update)
|
||||
delayables.append(session_user_sync)
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
self.report(
|
||||
@ -176,6 +192,47 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
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):
|
||||
bl_idname = "session.stop"
|
||||
bl_label = "close"
|
||||
@ -188,15 +245,18 @@ class SessionStopOperator(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
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"}
|
||||
|
||||
|
||||
class SessionKickOperator(bpy.types.Operator):
|
||||
bl_idname = "session.kick"
|
||||
bl_label = "Kick"
|
||||
@ -223,10 +283,10 @@ class SessionKickOperator(bpy.types.Operator):
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
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):
|
||||
bl_idname = "session.remove_prop"
|
||||
@ -319,7 +379,8 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
||||
wm.event_timer_remove(self._timer)
|
||||
|
||||
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:
|
||||
self.cancel(context)
|
||||
@ -334,11 +395,26 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
||||
|
||||
if target_ref:
|
||||
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(
|
||||
target_ref['metadata']['view_matrix'])
|
||||
# Handle client on other scenes
|
||||
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:
|
||||
return {"CANCELLED"}
|
||||
|
||||
@ -462,7 +538,7 @@ class ApplyArmatureOperator(bpy.types.Operator):
|
||||
try:
|
||||
client.apply(node)
|
||||
except Exception as e:
|
||||
logging.error("Dail to apply armature: {e}")
|
||||
logging.error("Fail to apply armature: {e}")
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
@ -492,10 +568,26 @@ classes = (
|
||||
SessionCommit,
|
||||
ApplyArmatureOperator,
|
||||
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
|
||||
def load_pre_handler(dummy):
|
||||
global client
|
||||
@ -504,9 +596,6 @@ def load_pre_handler(dummy):
|
||||
bpy.ops.session.stop()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@persistent
|
||||
def update_client_frame(scene):
|
||||
if client and client.state['STATE'] == STATE_ACTIVE:
|
||||
@ -553,9 +642,10 @@ def register():
|
||||
for cls in classes:
|
||||
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.frame_change_pre.append(update_client_frame)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
|
||||
@ -572,8 +662,9 @@ def unregister():
|
||||
for cls in reversed(classes):
|
||||
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.frame_change_pre.remove(update_client_frame)
|
||||
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation)
|
@ -19,10 +19,12 @@ import random
|
||||
import logging
|
||||
import bpy
|
||||
import string
|
||||
import re
|
||||
|
||||
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():
|
||||
@ -44,6 +46,22 @@ def update_panel_category(self, context):
|
||||
ui.SESSION_PT_settings.bl_category = self.panel_category
|
||||
ui.register()
|
||||
|
||||
def update_ip(self, context):
|
||||
ip = IP_EXPR.search(self.ip)
|
||||
|
||||
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):
|
||||
type_name: bpy.props.StringProperty()
|
||||
@ -68,7 +86,8 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
ip: bpy.props.StringProperty(
|
||||
name="ip",
|
||||
description='Distant host ip',
|
||||
default="127.0.0.1")
|
||||
default="127.0.0.1",
|
||||
update=update_ip)
|
||||
username: bpy.props.StringProperty(
|
||||
name="Username",
|
||||
default=f"user_{random_string_digits()}"
|
||||
@ -91,19 +110,16 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
ipc_port: bpy.props.IntProperty(
|
||||
name="ipc_port",
|
||||
description='internal ttl port(only usefull for multiple local instances)',
|
||||
default=5561
|
||||
default=5561,
|
||||
update=update_port
|
||||
)
|
||||
start_empty: bpy.props.BoolProperty(
|
||||
name="start_empty",
|
||||
default=False
|
||||
)
|
||||
right_strategy: bpy.props.EnumProperty(
|
||||
name='right_strategy',
|
||||
description='right strategy',
|
||||
init_method: bpy.props.EnumProperty(
|
||||
name='init_method',
|
||||
description='Init repo',
|
||||
items={
|
||||
('STRICT', 'strict', 'strict right repartition'),
|
||||
('COMMON', 'common', 'relaxed right repartition')},
|
||||
default='COMMON')
|
||||
('EMPTY', 'an empty scene', 'start empty'),
|
||||
('BLEND', 'current scenes', 'use current scenes')},
|
||||
default='BLEND')
|
||||
cache_directory: bpy.props.StringProperty(
|
||||
name="cache directory",
|
||||
subtype="DIR_PATH",
|
||||
@ -236,8 +252,8 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
row.label(text="Port:")
|
||||
row.prop(self, "port", text="Address")
|
||||
row = box.row()
|
||||
row.label(text="Start with an empty scene:")
|
||||
row.prop(self, "start_empty", text="")
|
||||
row.label(text="Init the session from:")
|
||||
row.prop(self, "init_method", text="")
|
||||
|
||||
table = box.box()
|
||||
table.row().prop(
|
||||
@ -264,10 +280,9 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
icon='DISCLOSURE_TRI_DOWN' if self.conf_session_hosting_expanded
|
||||
else 'DISCLOSURE_TRI_RIGHT', emboss=False)
|
||||
if self.conf_session_hosting_expanded:
|
||||
box.row().prop(self, "right_strategy", text="Right model")
|
||||
row = box.row()
|
||||
row.label(text="Start with an empty scene:")
|
||||
row.prop(self, "start_empty", text="")
|
||||
row.label(text="Init the session from:")
|
||||
row.prop(self, "init_method", text="")
|
||||
|
||||
# CACHE SETTINGS
|
||||
box = grid.box()
|
||||
@ -340,17 +355,13 @@ class SessionUser(bpy.types.PropertyGroup):
|
||||
|
||||
|
||||
class SessionProps(bpy.types.PropertyGroup):
|
||||
is_admin: bpy.props.BoolProperty(
|
||||
name="is_admin",
|
||||
default=False
|
||||
)
|
||||
session_mode: bpy.props.EnumProperty(
|
||||
name='session_mode',
|
||||
description='session mode',
|
||||
items={
|
||||
('HOST', 'hosting', 'host a session'),
|
||||
('CONNECT', 'connexion', 'connect to a session')},
|
||||
default='HOST')
|
||||
('HOST', 'HOST', 'host a session'),
|
||||
('CONNECT', 'JOIN', 'connect to a session')},
|
||||
default='CONNECT')
|
||||
clients: bpy.props.EnumProperty(
|
||||
name="clients",
|
||||
description="client enum",
|
||||
@ -374,7 +385,7 @@ class SessionProps(bpy.types.PropertyGroup):
|
||||
update=presence.update_overlay_settings
|
||||
)
|
||||
presence_show_far_user: bpy.props.BoolProperty(
|
||||
name="Show different scenes",
|
||||
name="Show users on different scenes",
|
||||
description="Show user on different scenes",
|
||||
default=False,
|
||||
update=presence.update_overlay_settings
|
||||
@ -384,12 +395,31 @@ class SessionProps(bpy.types.PropertyGroup):
|
||||
description='Show only owned datablocks',
|
||||
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(
|
||||
default=False
|
||||
)
|
||||
time_snap_running: bpy.props.BoolProperty(
|
||||
default=False
|
||||
)
|
||||
is_host: bpy.props.BoolProperty(
|
||||
default=False
|
||||
)
|
||||
|
||||
|
||||
classes = (
|
||||
|
@ -19,6 +19,7 @@
|
||||
import copy
|
||||
import logging
|
||||
import math
|
||||
import traceback
|
||||
|
||||
import bgl
|
||||
import blf
|
||||
@ -311,10 +312,10 @@ class DrawFactory(object):
|
||||
self.d2d_items[client_id] = (position[1], client_id, color)
|
||||
|
||||
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):
|
||||
bgl.glLineWidth(1.5)
|
||||
bgl.glLineWidth(2.)
|
||||
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
261
multi_user/ui.py
@ -19,12 +19,13 @@
|
||||
import bpy
|
||||
|
||||
from . import operators, utils
|
||||
from .libs.replication.replication.constants import (ADDED, ERROR, FETCHED,
|
||||
from replication.constants import (ADDED, ERROR, FETCHED,
|
||||
MODIFIED, RP_COMMON, UP,
|
||||
STATE_ACTIVE, STATE_AUTH,
|
||||
STATE_CONFIG, STATE_SYNCING,
|
||||
STATE_INITIAL, STATE_SRV_SYNC,
|
||||
STATE_WAITING, STATE_QUITTING,
|
||||
STATE_LOBBY,
|
||||
STATE_LAUNCHING_SERVICES)
|
||||
|
||||
ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
|
||||
@ -34,7 +35,8 @@ ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
|
||||
'FILE_REFRESH', # UP
|
||||
'TRIA_UP'] # CHANGED
|
||||
|
||||
def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█', fill_empty=' '):
|
||||
|
||||
def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█', fill_empty=' '):
|
||||
"""
|
||||
Call in a loop to create terminal progress bar
|
||||
@params:
|
||||
@ -48,16 +50,19 @@ def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1,
|
||||
From here:
|
||||
https://gist.github.com/greenstick/b23e475d2bfdc3a82e34eaa1f6781ee4
|
||||
"""
|
||||
if total == 0:
|
||||
return ""
|
||||
filledLength = int(length * iteration // total)
|
||||
bar = fill * filledLength + fill_empty * (length - filledLength)
|
||||
return f"{prefix} |{bar}| {iteration}/{total}{suffix}"
|
||||
|
||||
|
||||
def get_state_str(state):
|
||||
state_str = 'UNKNOWN'
|
||||
if state == STATE_WAITING:
|
||||
state_str = 'WARMING UP DATA'
|
||||
elif state == STATE_SYNCING:
|
||||
state_str = 'FETCHING FROM SERVER'
|
||||
state_str = 'FETCHING'
|
||||
elif state == STATE_AUTH:
|
||||
state_str = 'AUTHENTIFICATION'
|
||||
elif state == STATE_CONFIG:
|
||||
@ -65,31 +70,49 @@ def get_state_str(state):
|
||||
elif state == STATE_ACTIVE:
|
||||
state_str = 'ONLINE'
|
||||
elif state == STATE_SRV_SYNC:
|
||||
state_str = 'PUSHING TO SERVER'
|
||||
state_str = 'PUSHING'
|
||||
elif state == STATE_INITIAL:
|
||||
state_str = 'INIT'
|
||||
elif state == STATE_QUITTING:
|
||||
state_str = 'QUITTING SESSION'
|
||||
state_str = 'QUITTING'
|
||||
elif state == STATE_LAUNCHING_SERVICES:
|
||||
state_str = 'LAUNCHING SERVICES'
|
||||
elif state == STATE_LOBBY:
|
||||
state_str = 'LOBBY'
|
||||
|
||||
return state_str
|
||||
|
||||
|
||||
class SESSION_PT_settings(bpy.types.Panel):
|
||||
"""Settings panel"""
|
||||
bl_idname = "MULTIUSER_SETTINGS_PT_panel"
|
||||
bl_label = "Session"
|
||||
bl_label = " "
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Multiuser"
|
||||
|
||||
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):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
row = layout.row()
|
||||
runtime_settings = context.window_manager.session
|
||||
settings = utils.get_preferences()
|
||||
|
||||
if hasattr(context.window_manager, 'session'):
|
||||
# STATE INITIAL
|
||||
@ -99,25 +122,32 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
else:
|
||||
cli_state = operators.client.state
|
||||
|
||||
row.label(text=f"Status : {get_state_str(cli_state['STATE'])}")
|
||||
|
||||
row = layout.row()
|
||||
|
||||
current_state = cli_state['STATE']
|
||||
|
||||
# STATE ACTIVE
|
||||
if current_state == STATE_ACTIVE:
|
||||
if current_state in [STATE_ACTIVE]:
|
||||
row.operator("session.stop", icon='QUIT', text="Exit")
|
||||
row = layout.row()
|
||||
|
||||
if runtime_settings.is_host:
|
||||
row = row.box()
|
||||
row.label(text=f"LAN: {runtime_settings.internet_ip}", icon='INFO')
|
||||
row = layout.row()
|
||||
if current_state == STATE_LOBBY:
|
||||
row = row.box()
|
||||
row.label(text=f"Waiting the session to start", icon='INFO')
|
||||
row = layout.row()
|
||||
row.operator("session.stop", icon='QUIT', text="Exit")
|
||||
# CONNECTION STATE
|
||||
elif current_state in [
|
||||
STATE_SRV_SYNC,
|
||||
STATE_SYNCING,
|
||||
STATE_AUTH,
|
||||
STATE_CONFIG,
|
||||
STATE_WAITING]:
|
||||
elif current_state in [STATE_SRV_SYNC,
|
||||
STATE_SYNCING,
|
||||
STATE_AUTH,
|
||||
STATE_CONFIG,
|
||||
STATE_WAITING]:
|
||||
|
||||
if cli_state['STATE'] in [STATE_SYNCING,STATE_SRV_SYNC,STATE_WAITING]:
|
||||
if cli_state['STATE'] in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]:
|
||||
box = row.box()
|
||||
box.label(text=printProgressBar(
|
||||
cli_state['CURRENT'],
|
||||
@ -136,13 +166,15 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
if state == STATE_ACTIVE:
|
||||
num_online_services += 1
|
||||
|
||||
total_online_services = len(operators.client.services_state)
|
||||
total_online_services = len(
|
||||
operators.client.services_state)
|
||||
|
||||
box.label(text=printProgressBar(
|
||||
total_online_services-num_online_services,
|
||||
total_online_services,
|
||||
length=16
|
||||
))
|
||||
total_online_services-num_online_services,
|
||||
total_online_services,
|
||||
length=16
|
||||
))
|
||||
|
||||
|
||||
class SESSION_PT_settings_network(bpy.types.Panel):
|
||||
bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel"
|
||||
@ -156,6 +188,9 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
||||
return not operators.client \
|
||||
or (operators.client and operators.client.state['STATE'] == 0)
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='URL')
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@ -169,32 +204,39 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
||||
|
||||
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':
|
||||
row = box.row()
|
||||
row.label(text="Start empty:")
|
||||
row.prop(settings, "start_empty", text="")
|
||||
row.label(text="Port:")
|
||||
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.operator("session.start", text="HOST").host = True
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
class SESSION_PT_settings_user(bpy.types.Panel):
|
||||
bl_idname = "MULTIUSER_SETTINGS_USER_PT_panel"
|
||||
bl_label = "User"
|
||||
bl_label = "User info"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||
@ -204,6 +246,9 @@ class SESSION_PT_settings_user(bpy.types.Panel):
|
||||
return not operators.client \
|
||||
or (operators.client and operators.client.state['STATE'] == 0)
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='USER')
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@ -219,7 +264,7 @@ class SESSION_PT_settings_user(bpy.types.Panel):
|
||||
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_label = "Advanced"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
@ -232,26 +277,36 @@ class SESSION_PT_settings_replication(bpy.types.Panel):
|
||||
return not operators.client \
|
||||
or (operators.client and operators.client.state['STATE'] == 0)
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='PREFERENCES')
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
runtime_settings = context.window_manager.session
|
||||
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':
|
||||
row = layout.row()
|
||||
row.prop(settings.sync_flags,"sync_render_settings")
|
||||
replication_section_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()
|
||||
|
||||
row = layout.row()
|
||||
replication_section_row = replication_section.row()
|
||||
replication_section_row.label(text="Per data type timers:")
|
||||
replication_section_row = replication_section.row()
|
||||
# Replication frequencies
|
||||
flow = row .grid_flow(
|
||||
flow = replication_section_row .grid_flow(
|
||||
row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
|
||||
line = flow.row(align=True)
|
||||
line.label(text=" ")
|
||||
@ -276,45 +331,52 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
layout = self.layout
|
||||
online_users = context.window_manager.online_users
|
||||
selected_user = context.window_manager.user_index
|
||||
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
|
||||
|
||||
# Create a simple row.
|
||||
row = layout.row()
|
||||
box = row.box()
|
||||
split = box.split(factor=0.3)
|
||||
split = box.split(factor=0.35)
|
||||
split.label(text="user")
|
||||
split = split.split(factor=0.5)
|
||||
split.label(text="localisation")
|
||||
split.label(text="location")
|
||||
split.label(text="frame")
|
||||
split.label(text="ping")
|
||||
|
||||
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:
|
||||
row = layout.row()
|
||||
user_operations = row.split()
|
||||
user_operations.alert = context.window_manager.session.time_snap_running
|
||||
user_operations.operator(
|
||||
"session.snapview",
|
||||
text="",
|
||||
icon='VIEW_CAMERA').target_client = active_user.username
|
||||
if operators.client.state['STATE'] == STATE_ACTIVE:
|
||||
|
||||
user_operations.alert = context.window_manager.session.user_snap_running
|
||||
user_operations.operator(
|
||||
"session.snaptime",
|
||||
text="",
|
||||
icon='TIME').target_client = active_user.username
|
||||
user_operations.alert = context.window_manager.session.time_snap_running
|
||||
user_operations.operator(
|
||||
"session.snapview",
|
||||
text="",
|
||||
icon='VIEW_CAMERA').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(
|
||||
"session.kick",
|
||||
text="",
|
||||
@ -329,6 +391,7 @@ class SESSION_UL_users(bpy.types.UIList):
|
||||
ping = '-'
|
||||
frame_current = '-'
|
||||
scene_current = '-'
|
||||
status_icon = 'BLANK1'
|
||||
if session:
|
||||
user = session.online_users.get(item.username)
|
||||
if user:
|
||||
@ -337,8 +400,10 @@ class SESSION_UL_users(bpy.types.UIList):
|
||||
if metadata and 'frame_current' in metadata:
|
||||
frame_current = str(metadata['frame_current'])
|
||||
scene_current = metadata['scene_current']
|
||||
split = layout.split(factor=0.3)
|
||||
split.label(text=item.username)
|
||||
if user['admin']:
|
||||
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.label(text=scene_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])
|
||||
|
||||
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):
|
||||
layout = self.layout
|
||||
@ -367,11 +433,11 @@ class SESSION_PT_presence(bpy.types.Panel):
|
||||
settings = context.window_manager.session
|
||||
layout.active = settings.enable_presence
|
||||
col = layout.column()
|
||||
col.prop(settings,"presence_show_selected")
|
||||
col.prop(settings,"presence_show_user")
|
||||
col.prop(settings, "presence_show_selected")
|
||||
col.prop(settings, "presence_show_user")
|
||||
row = layout.column()
|
||||
row.active = settings.presence_show_user
|
||||
row.prop(settings,"presence_show_far_user")
|
||||
row.active = settings.presence_show_user
|
||||
row.prop(settings, "presence_show_far_user")
|
||||
|
||||
|
||||
class SESSION_PT_services(bpy.types.Panel):
|
||||
@ -386,12 +452,15 @@ class SESSION_PT_services(bpy.types.Panel):
|
||||
def poll(cls, context):
|
||||
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):
|
||||
layout = self.layout
|
||||
online_users = context.window_manager.online_users
|
||||
selected_user = context.window_manager.user_index
|
||||
settings = context.window_manager.session
|
||||
active_user = online_users[selected_user] if len(online_users)-1>=selected_user else 0
|
||||
active_user = online_users[selected_user] if len(online_users)-1 >= selected_user else 0
|
||||
|
||||
# Create a simple row.
|
||||
for name, state in operators.client.services_state.items():
|
||||
@ -400,7 +469,6 @@ class SESSION_PT_services(bpy.types.Panel):
|
||||
row.label(text=get_state_str(state))
|
||||
|
||||
|
||||
|
||||
def draw_property(context, parent, property_uuid, level=0):
|
||||
settings = utils.get_preferences()
|
||||
runtime_settings = context.window_manager.session
|
||||
@ -425,8 +493,7 @@ def draw_property(context, parent, property_uuid, level=0):
|
||||
|
||||
# Operations
|
||||
|
||||
have_right_to_modify = runtime_settings.is_admin or \
|
||||
item.owner == settings.username or \
|
||||
have_right_to_modify = item.owner == settings.username or \
|
||||
item.owner == RP_COMMON
|
||||
|
||||
if have_right_to_modify:
|
||||
@ -464,16 +531,27 @@ def draw_property(context, parent, property_uuid, level=0):
|
||||
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_label = "Properties"
|
||||
bl_label = "Repository"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
|
||||
@ -481,10 +559,16 @@ class SESSION_PT_outliner(bpy.types.Panel):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
if hasattr(context.window_manager, 'session'):
|
||||
# Filters
|
||||
settings = utils.get_preferences()
|
||||
runtime_settings = context.window_manager.session
|
||||
# Filters
|
||||
settings = utils.get_preferences()
|
||||
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(
|
||||
row_major=True,
|
||||
columns=0,
|
||||
@ -512,7 +596,7 @@ class SESSION_PT_outliner(bpy.types.Panel):
|
||||
if operators.client.get(uuid=key).str_type
|
||||
in types_filter]
|
||||
|
||||
if client_keys and len(client_keys) > 0:
|
||||
if client_keys:
|
||||
col = layout.column(align=True)
|
||||
for key in client_keys:
|
||||
draw_property(context, col, key)
|
||||
@ -520,6 +604,11 @@ class SESSION_PT_outliner(bpy.types.Panel):
|
||||
else:
|
||||
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 = (
|
||||
SESSION_UL_users,
|
||||
@ -527,10 +616,10 @@ classes = (
|
||||
SESSION_PT_settings_user,
|
||||
SESSION_PT_settings_network,
|
||||
SESSION_PT_presence,
|
||||
SESSION_PT_settings_replication,
|
||||
SESSION_PT_advanced_settings,
|
||||
SESSION_PT_user,
|
||||
SESSION_PT_services,
|
||||
SESSION_PT_outliner,
|
||||
SESSION_PT_repository,
|
||||
|
||||
)
|
||||
|
||||
|
@ -13,7 +13,7 @@ def main():
|
||||
if len(sys.argv) > 2:
|
||||
blender_rev = sys.argv[2]
|
||||
else:
|
||||
blender_rev = "2.82"
|
||||
blender_rev = "2.90.0"
|
||||
|
||||
try:
|
||||
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',
|
||||
'LIMIT_LOCATION', 'LIMIT_ROTATION', 'LIMIT_SCALE', 'MAINTAIN_VOLUME',
|
||||
'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']
|
||||
|
||||
#temporary disabled 'SPLINE_IK' until its fixed
|
||||
|
||||
def test_object(clear_blend):
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
enter_editmode=False, align='WORLD', location=(0, 0, 0))
|
||||
|