Compare commits

...

94 Commits

Author SHA1 Message Date
45437660ba clean: remove unused lock 2020-10-22 17:37:53 +02:00
ee93a5b209 Merge branch 'develop' into 132-fix-undo-edit-last-operation-redo-handling 2020-10-22 16:21:31 +02:00
f90c12b27f doc: added missing fields
feat: changed session widget defaults
2020-10-22 16:07:19 +02:00
3573db0969 Merge branch '134-revamp-session-status-ui-widget' into 'develop'
Resolve "Revamp session status UI widget"

See merge request slumber/multi-user!67
2020-10-22 13:52:29 +00:00
92bde00a5a feat: store session widget settings to preferences 2020-10-22 15:48:13 +02:00
2c82560d24 fix: grease pencil material 2020-10-22 13:55:26 +02:00
6f364d2b88 feat: session widget position and scale settings
feat: ui_scale is now taken in account for session widget text size
2020-10-21 23:33:44 +02:00
760b52c02b Merge branch '135-empty-and-light-objects-user-selection-highlight-is-broken' into 'develop'
Resolve "Empty and Light objects user selection highlight is broken"

See merge request slumber/multi-user!66
2020-10-21 15:25:42 +00:00
4dd932fc56 fix: empty and light display broken 2020-10-21 17:23:59 +02:00
ba1a03cbfa Merge branch '133-material-renaming-is-unstable' into 'develop'
Resolve "Material renaming is unstable"

See merge request slumber/multi-user!65
2020-10-21 13:17:18 +00:00
18b5fa795c feat: resolve materials from uuid by default and fallback on regular name resolving 2020-10-21 15:10:37 +02:00
1a82ec72e4 fix: change owner call in opterator 2020-10-21 14:40:15 +02:00
804747c73b fix: owning parent when a child is already owned (ex: duplicate linked) 2020-10-21 14:15:42 +02:00
7ee705332f feat: update replication to prevent UnpicklingError from crashing the network Thred 2020-10-20 17:25:50 +02:00
4bd0055056 Merge branch 'develop' into 132-fix-undo-edit-last-operation-redo-handling 2020-10-16 14:57:36 +02:00
716c78e380 feat: update changelog 2020-10-16 11:06:41 +02:00
5e4ce4556f doc: update operator descriptions 2020-10-16 10:57:45 +02:00
aa9ea08151 doc: update refresh icon 2020-10-16 10:28:29 +02:00
f56890128e fix: material test by splitting it in a gpencil and nodal material test 2020-10-15 18:08:08 +02:00
8865556229 feat: update CHANGELOG 2020-10-15 18:02:07 +02:00
5bc9b10c12 fix: material gpencil loading 2020-10-15 18:01:54 +02:00
7db3c18213 feat: affect dependencies option in change owner 2020-10-15 17:48:04 +02:00
f151c61d7b feat: mimic blender undo handling 2020-10-15 17:21:14 +02:00
ff35e34032 feat: update apply ui icon
fix: material property filter
2020-10-15 17:09:50 +02:00
9f8222afa7 fix: handle apply dependencies 2020-10-15 12:11:28 +02:00
1828bfac22 feat: update changelog 2020-10-14 19:25:59 +02:00
3a1087ecb8 Merge branch '131-sync-render-settings-flag-cause-a-race-condition' into 'develop'
Resolve "Sync render settings flag cause a race condition"

See merge request slumber/multi-user!63
2020-10-14 17:16:20 +00:00
b398541787 fix: apply operator 2020-10-14 19:12:28 +02:00
f0b33d8471 fix: race condition in scene sync 2020-10-14 19:11:32 +02:00
5a282a3e22 Merge branch '130-mesh-transfert-is-broken-between-a-hybrid-linux-windows-session' into 'develop'
Resolve "Mesh transfert is broken between a hybrid linux-windows session"

See merge request slumber/multi-user!62
2020-10-14 14:07:59 +00:00
4283fc0fff fix: crash during hybrid session
Related to #130
2020-10-14 16:06:11 +02:00
753f4d3f27 fix: prevent NonAuthorizedOperationError to kill the right managment timer 2020-10-14 00:36:59 +02:00
9dd02b2756 feat: fix binary diff 2020-10-13 17:15:31 +02:00
c74d12c843 fix: handle world empty dependencies 2020-10-13 17:10:25 +02:00
e1d9982276 fix: bl_file diff when clear memory cache is enabled 2020-10-13 17:09:43 +02:00
8861986213 fix: packed image save error 2020-10-13 16:58:48 +02:00
1cb9fb410c feat: material node output default value support
fix: prevent material empty dependencies
2020-10-12 23:10:42 +02:00
c4a8cc4606 Merge branch 'fix_deploy' into 'develop'
Fix deploy

See merge request slumber/multi-user!61
2020-10-12 19:03:47 +00:00
187f11071c feat: enable build and deploy for only master and develop 2020-10-12 21:01:54 +02:00
530fae8cb4 feat: active deploy 2020-10-12 20:24:12 +02:00
6771c371a1 feat: enable deploy back 2020-10-12 20:23:08 +02:00
c844c6e54f clean: keep only active renderer settings (bl_scene.py)
fix: resolve_deps_implementation now only resolve master collection objects (bl_scene.py)
2020-10-12 20:21:08 +02:00
a4d0b1a68b fix: client selection 2020-10-12 18:56:42 +02:00
2fdc11692d fix: handle None bounding box position 2020-10-12 18:15:59 +02:00
dbfca4568f fix: get_preference import syntax 2020-10-12 18:07:09 +02:00
069a528276 feat: test scene with sync render settings enabled 2020-10-12 18:04:54 +02:00
030f2661fd fix: buffer empty for the first diff 2020-10-12 17:13:35 +02:00
e589e3eec4 fix: file not found logging
clean: remove cache for scene diff
2020-10-12 17:12:50 +02:00
04140ced1b fix: collection instance bounding box display 2020-10-12 17:11:46 +02:00
0d9ce43e74 fix: enable binrary differentialback
feat: ignore material node bl_label
2020-10-12 13:33:49 +02:00
d3969b4fd4 Revert "feat: avoid dumping read only properties"
This reverts commit cefaef5c4b.
2020-10-12 10:23:19 +02:00
e21f64ac98 revert: bl_label 2020-10-11 19:20:53 +02:00
b25b380d21 fix: missing bl_idname 2020-10-11 19:11:51 +02:00
1146d9d304 feat: disable render settings sync by default 2020-10-11 19:08:06 +02:00
51b60521e6 feat: update relplication version 2020-10-11 19:07:48 +02:00
035f8a1dcd feat: skipping not required parameters 2020-10-11 19:07:28 +02:00
cefaef5c4b feat: avoid dumping read only properties 2020-10-11 19:06:58 +02:00
4714e60ff7 Merge branch 'develop' of gitlab.com:slumber/multi-user into develop 2020-10-11 15:22:05 +02:00
3eca25ae19 feat: update replication version 2020-10-11 15:10:28 +02:00
96346f8a25 refactor: clean debug logs 2020-10-11 15:06:32 +02:00
a258c2c182 Merge branch 'feature/doc-updates-2' into 'develop'
Feature/doc updates 2

See merge request slumber/multi-user!60
2020-10-09 09:28:36 +00:00
6862df5331 Minor doc update 2020-10-09 01:59:42 +02:00
f271a9d0e3 Updated contribution doc to indicate how to sync with upstream repository 2020-10-09 01:55:45 +02:00
bdff6eb5c9 Updated contribution documentation with how to sync upstream repo 2020-10-09 01:29:01 +02:00
b661407952 Merge branch '128-ui-gizmo-error' into 'develop'
Resolve "UI gizmo error"

See merge request slumber/multi-user!59
2020-10-08 22:50:11 +00:00
d5eb7fda02 fix: ci yaml error 2020-10-09 00:46:52 +02:00
35e8ac9c33 feat: disable deploy until fixed 2020-10-09 00:45:30 +02:00
4453d256b8 feat: update replication version, switched dependency to pyzmq 2020-10-08 23:57:39 +02:00
299e330ec6 fix: internal gizmo error by launching the modal operator from the timer 2020-10-08 23:42:14 +02:00
34b9f7ae27 Merge branch 'master' into develop 2020-10-08 23:14:58 +02:00
9d100d84ad Merge branch 'hotfix/ui-spelling-fixes' into 'master'
Hotfix/ui spelling fixes

See merge request slumber/multi-user!58
2020-10-08 20:58:13 +00:00
2f677c399e UI spelling fixes to preferences.py and ui.py 2020-10-08 22:52:24 +02:00
e967b35c38 Revert "Minor UI spelling errors"
This reverts commit 673c4e69a4.
2020-10-08 21:58:30 +02:00
7bd0a196b4 Merge branch 'feature/doc-updates' into 'develop'
Feature/doc updates

See merge request slumber/multi-user!57
2020-10-08 17:04:37 +00:00
7892b5e9b6 Adding log-level to server startup scripts 2020-10-08 18:35:08 +02:00
f779678c0e Updates to hosting guide and contribution documentation 2020-10-08 18:31:20 +02:00
629fc2d223 feat: update dockerfile 2020-10-08 15:10:32 +02:00
724c2345df refactor: disable force apply during the reparent 2020-10-08 15:00:27 +02:00
673c4e69a4 Minor UI spelling errors 2020-10-08 00:31:56 +02:00
fbfff6c7ec Doc updates clarifying developer workflow, updating hosting instructions 2020-10-08 00:08:23 +02:00
f592294335 Added scripts to conveniently start server instance via docker or replication 2020-10-07 21:20:43 +02:00
8e7be5afde Merge branch '126-draw-refactoring' into 'develop'
Resolve "Draw refactoring"

See merge request slumber/multi-user!55
2020-10-06 14:12:13 +00:00
fc76b2a8e6 fix: avoid to remove inexistant user widget 2020-10-06 16:10:10 +02:00
1a8bcddb74 refactor: formatting 2020-10-06 15:53:29 +02:00
60fba5b9df refactor: use dict to store widgets 2020-10-06 15:46:35 +02:00
be0eb1fa42 clean: remove unused import 2020-10-06 09:45:13 +02:00
93d9bea3ae feat: display session status 2020-10-05 23:38:52 +02:00
022b7f7822 refactor: enable username display again
refactor: avoid to draw the local user
2020-10-05 22:34:43 +02:00
ae34846509 fix: ci syntax 2020-10-05 21:53:14 +02:00
d328077cb0 feat: deploy and build only for master and develop
refactor: carry on presence refactoring
2020-10-05 21:51:54 +02:00
0c4740eef8 fix: import error 2020-10-05 18:48:40 +02:00
d7b2c7e2f6 refactor: started to rewrite presence
fix: weird bounding boxes on various objects types

Related to #55
2020-10-05 18:34:41 +02:00
efbb9e7096 doc: feat changelog 0.1.0 release date 2020-10-05 16:11:04 +02:00
7a94c21187 doc: update version 2020-10-05 15:37:06 +02:00
32 changed files with 1218 additions and 569 deletions

View File

@ -3,7 +3,8 @@ stages:
- build - build
- deploy - deploy
include: include:
- local: .gitlab/ci/test.gitlab-ci.yml - local: .gitlab/ci/test.gitlab-ci.yml
- local: .gitlab/ci/build.gitlab-ci.yml - local: .gitlab/ci/build.gitlab-ci.yml
- local: .gitlab/ci/deploy.gitlab-ci.yml - local: .gitlab/ci/deploy.gitlab-ci.yml

View File

@ -7,4 +7,7 @@ build:
name: multi_user name: multi_user
paths: paths:
- multi_user - multi_user
only:
refs:
- master
- develop

View File

@ -16,3 +16,8 @@ deploy:
- echo "Pushing to gitlab registry ${VERSION}" - echo "Pushing to gitlab registry ${VERSION}"
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION} - docker push registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION}
only:
refs:
- master
- develop

View File

@ -65,7 +65,7 @@ All notable changes to this project will be documented in this file.
- Unused strict right management strategy - Unused strict right management strategy
- Legacy config management system - Legacy config management system
## [0.1.0] - preview ## [0.1.0] - 2020-10-05
### Added ### Added
@ -95,4 +95,33 @@ All notable changes to this project will be documented in this file.
- Modifier vertex group assignation - Modifier vertex group assignation
- World sync - World sync
- Snapshot UUID error - Snapshot UUID error
- The world is not synchronized - The world is not synchronized
## [0.1.1] - 2020-10-16
### Added
- Session status widget
- Affect dependencies during change owner
- Dedicated server managment scripts(@brybalicious)
### Changed
- Refactored presence.py
- Reset button UI icon
- Documentation `How to contribute` improvements (@brybalicious)
- Documentation `Hosting guide` improvements (@brybalicious)
- Show flags are now available from the viewport overlay
### Fixed
- Render sync race condition (causing scene errors)
- Binary differentials
- Hybrid session crashes between Linux/Windows
- Materials node default output value
- Right selection
- Client node rights changed to COMMON after disconnecting from the server
- Collection instances selection draw
- Packed image save error
- Material replication
- UI spelling errors (@brybalicious)

View File

@ -22,7 +22,7 @@ copyright = '2020, Swann Martinez'
author = 'Swann Martinez' author = 'Swann Martinez'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '0.0.2' release = '0.1.0'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -251,6 +251,14 @@ it draw users related information in your viewport such as:
The presence overlay panel (see image above) allow you to enable/disable The presence overlay panel (see image above) allow you to enable/disable
various drawn parts via the following flags: various drawn parts via the following flags:
- **Show session statut**: display the session status in the viewport
.. figure:: img/quickstart_status.png
:align: center
- **Text scale**: session status text size
- **Vertical/Horizontal position**: session position in the viewport
- **Show selected objects**: display other users current selection - **Show selected objects**: display other users current selection
- **Show users**: display users current viewpoint - **Show users**: display users current viewpoint
- **Show different scenes**: display users working on other scenes - **Show different scenes**: display users working on other scenes

View File

@ -144,7 +144,7 @@ Let's check the connection status. Right click on the tray icon and click on **S
Network status. 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. 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. If you see something like **ACCESS_DENIED**, it means that you were not authorized to join the network. Please check the section :ref:`network-authorization`
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 ! 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 !
@ -171,26 +171,28 @@ From the dedicated server
run it at home for LAN but for internet hosting you need to follow the :ref:`port-forwarding` setup first. 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. The dedicated server allow you to host a session with simplicity from any location.
It was developed to improve intaernet hosting performance. It was developed to improve internet hosting performance.
The dedicated server can be run in tow ways: The dedicated server can be run in two ways:
- :ref:`cmd-line` - :ref:`cmd-line`
- :ref:`docker` - :ref:`docker`
.. Note:: There are shell scripts to conveniently start a dedicated server via either of these approaches available in the gitlab repository. See section: :ref:`serverstartscripts`
.. _cmd-line: .. _cmd-line:
Using a regular command line Using a regular command line
---------------------------- ----------------------------
You can run the dedicated server on any platform by following those steps: You can run the dedicated server on any platform by following these steps:
1. Firstly, download and intall python 3 (3.6 or above). 1. Firstly, download and intall python 3 (3.6 or above).
2. Install the replication library: 2. Install the latest version of the replication library:
.. code-block:: bash .. code-block:: bash
python -m pip install replication python -m pip install replication==0.0.21a15
4. Launch the server with: 4. Launch the server with:
@ -199,17 +201,20 @@ You can run the dedicated server on any platform by following those steps:
replication.serve replication.serve
.. hint:: .. hint::
You can also specify a custom **port** (-p), **timeout** (-t), **admin password** (-pwd), **log level(ERROR, WARNING, INFO or DEBUG)** (-l) and **log file** (-lf) with the following optionnal argument You can also specify a custom **port** (-p), **timeout** (-t), **admin password** (-pwd), **log level (ERROR, WARNING, INFO or DEBUG)** (-l) and **log file** (-lf) with the following optional arguments
.. code-block:: bash .. code-block:: bash
replication.serve -p 5555 -pwd toto -t 1000 -l INFO -lf server.log replication.serve -p 5555 -pwd admin -t 1000 -l INFO -lf server.log
Here, for example, a server is instantiated on port 5555, with password 'admin', a 1 second timeout, and logging enabled.
As soon as the dedicated server is running, you can connect to it from blender by following :ref:`how-to-join`.
As soon as the dedicated server is running, you can connect to it from blender (follow :ref:`how-to-join`).
.. hint:: .. hint::
Some commands are available to manage the session. Check :ref:`dedicated-management` to learn more. Some commands are available to enable an administrator to manage the session. Check :ref:`dedicated-management` to learn more.
.. _docker: .. _docker:
@ -217,22 +222,56 @@ As soon as the dedicated server is running, you can connect to it from blender (
Using a pre-configured image on docker engine Using a pre-configured image on docker engine
--------------------------------------------- ---------------------------------------------
Launching the dedicated server from a docker server is simple as: Launching the dedicated server from a docker server is simple as running:
.. code-block:: bash .. code-block:: bash
docker run -d \ docker run -d \
-p 5555-5560:5555-5560 \ -p 5555-5560:5555-5560 \
-e port=5555 \ -e port=5555 \
-e log_level=DEBUG \
-e password=admin \ -e password=admin \
-e timeout=1000 \ -e timeout=1000 \
registry.gitlab.com/slumber/multi-user/multi-user-server:0.0.3 registry.gitlab.com/slumber/multi-user/multi-user-server:0.1.0
As soon as the dedicated server is running, you can connect to it from blender. As soon as the dedicated server is running, you can connect to it from blender by following :ref:`how-to-join`.
You can check the :ref:`how-to-join` section.
You can check your container is running, and find its ID with:
.. code-block:: bash
docker ps
Logs for the server running in the docker container can be accessed by outputting the following to a log file:
.. code-block:: bash
docker log your-container-id >& dockerserver.log
.. Note:: If using WSL2 on Windows 10 (Windows Subsystem for Linux), it is preferable to run a dedicated server via regular command line approach (or the associated startup script) from within Windows - docker desktop for windows 10 usually uses the WSL2 backend where it is available.
.. _serverstartscripts:
Server startup scripts
----------------------
Convenient scripts are available in the Gitlab repository: https://gitlab.com/slumber/multi-user/scripts/startup_scripts/
Simply run the relevant script in a shell on the host machine to start a server with one line of code via replication directly or via a docker container. Choose between the two methods:
.. code-block:: bash
./start-server.sh
or
.. code-block:: bash
./run-dockerfile.sh
.. hint:: .. hint::
Some commands are available to manage the session. Check :ref:`dedicated-management` to learn more. Once your server is up and running, some commands are available to manage the session :ref:`dedicated-management`
.. _dedicated-management: .. _dedicated-management:

View File

@ -21,11 +21,11 @@ In order to help with the testing, you have several possibilities:
- Test `development branch <https://gitlab.com/slumber/multi-user/-/branches>`_ - Test `development branch <https://gitlab.com/slumber/multi-user/-/branches>`_
-------------------------- --------------------------
Filling an issue on Gitlab Filing an issue on Gitlab
-------------------------- --------------------------
The `gitlab issue tracker <https://gitlab.com/slumber/multi-user/issues>`_ is used for bug report and enhancement suggestion. The `gitlab issue tracker <https://gitlab.com/slumber/multi-user/issues>`_ is used for bug report and enhancement suggestion.
You will need a Gitlab account to be able to open a new issue there and click on "New issue" button. You will need a Gitlab account to be able to open a new issue there and click on "New issue" button in the main multi-user project.
Here are some useful information you should provide in a bug report: Here are some useful information you should provide in a bug report:
@ -35,8 +35,75 @@ Here are some useful information you should provide in a bug report:
Contributing code Contributing code
================= =================
1. Fork it (https://gitlab.com/yourname/yourproject/fork) In general, this project follows the `Gitflow Workflow <https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow>`_. It may help to understand that there are three different repositories - the upstream (main multi-user project repository, designated in git by 'upstream'), remote (forked repository, designated in git by 'origin'), and the local repository on your machine.
2. Create your feature branch (git checkout -b feature/fooBar) The following example suggests how to contribute a feature.
3. Commit your changes (git commit -am 'Add some fooBar')
4. Push to the branch (git push origin feature/fooBar) 1. Fork the project into a new repository:
5. Create a new Pull Request https://gitlab.com/yourname/multi-user
2. Clone the new repository locally:
.. code-block:: bash
git clone https://gitlab.com/yourname/multi-user.git
3. Keep your fork in sync with the main repository by setting up the upstream pointer once. cd into your git repo and then run:
.. code-block:: bash
git remote add upstream https://gitlab.com/slumber/multi-user.git
4. Now, locally check out the develop branch, upon which to base your new feature branch:
.. code-block:: bash
git checkout develop
5. Fetch any changes from the main upstream repository into your fork (especially if some time has passed since forking):
.. code-block:: bash
git fetch upstream
'Fetch' downloads objects and refs from the repository, but doesnt apply them to the branch we are working on. We want to apply the updates to the branch we will work from, which we checked out in step 4.
6. Let's merge any recent changes from the remote upstream (original repository's) 'develop' branch into our local 'develop' branch:
.. code-block:: bash
git merge upstream/develop
7. Update your forked repository's remote 'develop' branch with the fetched changes, just to keep things tidy. Make sure you haven't committed any local changes in the interim:
.. code-block:: bash
git push origin develop
8. Locally create your own new feature branch from the develop branch, using the syntax:
.. code-block:: bash
git checkout -b feature/yourfeaturename
...where 'feature/' designates a feature branch, and 'yourfeaturename' is a name of your choosing
9. Add and commit your changes, including a commit message:
.. code-block:: bash
git commit -am 'Add fooBar'
10. Push committed changes to the remote copy of your new feature branch which will be created in this step:
.. code-block:: bash
git push -u origin feature/yourfeaturename
If it's been some time since performing steps 4 through 7, make sure to checkout 'develop' again and pull the latest changes from upstream before checking out and creating feature/yourfeaturename and pushing changes. Alternatively, checkout 'feature/yourfeaturename' and simply run:
.. code-block:: bash
git rebase upstream/develop
and your staged commits will be merged along with the changes. More information on `rebasing here <https://git-scm.com/book/en/v2/Git-Branching-Rebasing>`_
.. Hint:: -u option sets up your locally created new branch to follow a remote branch which is now created with the same name on your remote repository.
11. Finally, create a new Pull/Merge Request on Gitlab to merge the remote version of this new branch with commited updates, back into the upstream develop branch, finalising the integration of the new feature.
12. Thanks for contributing!
.. Note:: For hotfixes, replace 'feature/' with 'hotfix/' and base the new branch off the parent 'master' branch instead of 'develop' branch. Make sure to checkout 'master' before running step 8
.. Note:: Let's follow the Atlassian `Gitflow Workflow <https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow>`_, except for one main difference - submitting a pull request rather than merging by ourselves.
.. Note:: See `here <https://philna.sh/blog/2018/08/21/git-commands-to-keep-a-fork-up-to-date/>`_ or `here <https://stefanbauer.me/articles/how-to-keep-your-git-fork-up-to-date>`_ for instructions on how to keep a fork up to date.

View File

@ -19,7 +19,7 @@
bl_info = { bl_info = {
"name": "Multi-User", "name": "Multi-User",
"author": "Swann Martinez", "author": "Swann Martinez",
"version": (0, 1, 0), "version": (0, 2, 0),
"description": "Enable real-time collaborative workflow inside blender", "description": "Enable real-time collaborative workflow inside blender",
"blender": (2, 82, 0), "blender": (2, 82, 0),
"location": "3D View > Sidebar > Multi-User tab", "location": "3D View > Sidebar > Multi-User tab",
@ -40,11 +40,11 @@ import sys
import bpy import bpy
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from . import environment, utils from . import environment
DEPENDENCIES = { DEPENDENCIES = {
("replication", '0.0.21a15'), ("replication", '0.2.0'),
} }

View File

@ -92,7 +92,6 @@ def load_driver(target_datablock, src_driver):
def get_datablock_from_uuid(uuid, default, ignore=[]): def get_datablock_from_uuid(uuid, default, ignore=[]):
if not uuid: if not uuid:
return default return default
for category in dir(bpy.data): for category in dir(bpy.data):
root = getattr(bpy.data, category) root = getattr(bpy.data, category)
if isinstance(root, Iterable) and category not in ignore: if isinstance(root, Iterable) and category not in ignore:
@ -123,14 +122,14 @@ class BlDatablock(ReplicatedDatablock):
# TODO: use is_library_indirect # TODO: use is_library_indirect
self.is_library = (instance and hasattr(instance, 'library') and self.is_library = (instance and hasattr(instance, 'library') and
instance.library) or \ instance.library) or \
(self.data and 'library' in self.data) (hasattr(self,'data') and self.data and 'library' in self.data)
if instance and hasattr(instance, 'uuid'): if instance and hasattr(instance, 'uuid'):
instance.uuid = self.uuid instance.uuid = self.uuid
self.diff_method = DIFF_BINARY # self.diff_method = DIFF_BINARY
def resolve(self): def resolve(self, construct = True):
datablock_ref = None datablock_ref = None
datablock_root = getattr(bpy.data, self.bl_id) datablock_root = getattr(bpy.data, self.bl_id)
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root) datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
@ -139,15 +138,19 @@ class BlDatablock(ReplicatedDatablock):
try: try:
datablock_ref = datablock_root[self.data['name']] datablock_ref = datablock_root[self.data['name']]
except Exception: except Exception:
name = self.data.get('name') if construct:
logging.debug(f"Constructing {name}") name = self.data.get('name')
datablock_ref = self._construct(data=self.data) logging.debug(f"Constructing {name}")
datablock_ref = self._construct(data=self.data)
if datablock_ref: if datablock_ref is not None:
setattr(datablock_ref, 'uuid', self.uuid) setattr(datablock_ref, 'uuid', self.uuid)
self.instance = datablock_ref
return True
else:
return False
self.instance = datablock_ref
def remove_instance(self): def remove_instance(self):
""" """
Remove instance from blender data Remove instance from blender data

View File

@ -65,7 +65,7 @@ class BlFile(ReplicatedDatablock):
self.instance = kwargs.get('instance', None) self.instance = kwargs.get('instance', None)
if self.instance and not self.instance.exists(): if self.instance and not self.instance.exists():
raise FileNotFoundError(self.instance) raise FileNotFoundError(str(self.instance))
self.preferences = utils.get_preferences() self.preferences = utils.get_preferences()
self.diff_method = DIFF_BINARY self.diff_method = DIFF_BINARY
@ -135,6 +135,9 @@ class BlFile(ReplicatedDatablock):
file.close() file.close()
def diff(self): def diff(self):
memory_size = sys.getsizeof(self.data['file'])-33 if self.preferences.clear_memory_filecache:
disk_size = self.instance.stat().st_size return False
return memory_size == disk_size else:
memory_size = sys.getsizeof(self.data['file'])-33
disk_size = self.instance.stat().st_size
return memory_size == disk_size

View File

@ -107,7 +107,7 @@ class BlImage(BlDatablock):
if self.instance.packed_file: if self.instance.packed_file:
filename = Path(bpy.path.abspath(self.instance.filepath)).name filename = Path(bpy.path.abspath(self.instance.filepath)).name
self.instance.filepath = get_filepath(filename) self.instance.filepath_raw = get_filepath(filename)
self.instance.save() self.instance.save()
# An image can't be unpacked to the modified path # An image can't be unpacked to the modified path
# TODO: make a bug report # TODO: make a bug report

View File

@ -26,6 +26,7 @@ from .bl_datablock import BlDatablock, get_datablock_from_uuid
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]') NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
def load_node(node_data, node_tree): def load_node(node_data, node_tree):
""" Load a node into a node_tree from a dict """ Load a node into a node_tree from a dict
@ -41,7 +42,7 @@ def load_node(node_data, node_tree):
image_uuid = node_data.get('image_uuid', None) image_uuid = node_data.get('image_uuid', None)
if image_uuid and not target_node.image: if image_uuid and not target_node.image:
target_node.image = get_datablock_from_uuid(image_uuid,None) target_node.image = get_datablock_from_uuid(image_uuid, None)
for input in node_data["inputs"]: for input in node_data["inputs"]:
if hasattr(target_node.inputs[input], "default_value"): if hasattr(target_node.inputs[input], "default_value"):
@ -51,6 +52,14 @@ def load_node(node_data, node_tree):
logging.error( logging.error(
f"Material {input} parameter not supported, skipping") f"Material {input} parameter not supported, skipping")
for output in node_data["outputs"]:
if hasattr(target_node.outputs[output], "default_value"):
try:
target_node.outputs[output].default_value = node_data["outputs"][output]["default_value"]
except:
logging.error(
f"Material {output} parameter not supported, skipping")
def load_links(links_data, node_tree): def load_links(links_data, node_tree):
""" Load node_tree links from a list """ Load node_tree links from a list
@ -62,8 +71,10 @@ def load_links(links_data, node_tree):
""" """
for link in links_data: for link in links_data:
input_socket = node_tree.nodes[link['to_node']].inputs[int(link['to_socket'])] input_socket = node_tree.nodes[link['to_node']
output_socket = node_tree.nodes[link['from_node']].outputs[int(link['from_socket'])] ].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) node_tree.links.new(input_socket, output_socket)
@ -78,8 +89,10 @@ def dump_links(links):
links_data = [] links_data = []
for link in links: for link in links:
to_socket = NODE_SOCKET_INDEX.search(link.to_socket.path_from_id()).group(1) to_socket = NODE_SOCKET_INDEX.search(
from_socket = NODE_SOCKET_INDEX.search(link.from_socket.path_from_id()).group(1) link.to_socket.path_from_id()).group(1)
from_socket = NODE_SOCKET_INDEX.search(
link.from_socket.path_from_id()).group(1)
links_data.append({ links_data.append({
'to_node': link.to_node.name, 'to_node': link.to_node.name,
'to_socket': to_socket, 'to_socket': to_socket,
@ -105,6 +118,7 @@ def dump_node(node):
"show_expanded", "show_expanded",
"name_full", "name_full",
"select", "select",
"bl_label",
"bl_height_min", "bl_height_min",
"bl_height_max", "bl_height_max",
"bl_height_default", "bl_height_default",
@ -136,8 +150,17 @@ def dump_node(node):
input_dumper.include_filter = ["default_value"] input_dumper.include_filter = ["default_value"]
if hasattr(i, 'default_value'): if hasattr(i, 'default_value'):
dumped_node['inputs'][i.name] = input_dumper.dump( dumped_node['inputs'][i.name] = input_dumper.dump(i)
i)
dumped_node['outputs'] = {}
for i in node.outputs:
output_dumper = Dumper()
output_dumper.depth = 2
output_dumper.include_filter = ["default_value"]
if hasattr(i, 'default_value'):
dumped_node['outputs'][i.name] = output_dumper.dump(i)
if hasattr(node, 'color_ramp'): if hasattr(node, 'color_ramp'):
ramp_dumper = Dumper() ramp_dumper = Dumper()
ramp_dumper.depth = 4 ramp_dumper.depth = 4
@ -162,6 +185,12 @@ def dump_node(node):
return dumped_node return dumped_node
def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
has_image = lambda node : (node.type in ['TEX_IMAGE', 'TEX_ENVIRONMENT'] and node.image)
return [node.image for node in node_tree.nodes if has_image(node)]
class BlMaterial(BlDatablock): class BlMaterial(BlDatablock):
bl_id = "materials" bl_id = "materials"
bl_class = bpy.types.Material bl_class = bpy.types.Material
@ -176,22 +205,22 @@ class BlMaterial(BlDatablock):
def _load_implementation(self, data, target): def _load_implementation(self, data, target):
loader = Loader() loader = Loader()
target.name = data['name']
if data['is_grease_pencil']: is_grease_pencil = data.get('is_grease_pencil')
use_nodes = data.get('use_nodes')
loader.load(target, data)
if is_grease_pencil:
if not target.is_grease_pencil: if not target.is_grease_pencil:
bpy.data.materials.create_gpencil_data(target) bpy.data.materials.create_gpencil_data(target)
loader.load(target.grease_pencil, data['grease_pencil'])
loader.load( elif use_nodes:
target.grease_pencil, data['grease_pencil'])
if data["use_nodes"]:
if target.node_tree is None: if target.node_tree is None:
target.use_nodes = True target.use_nodes = True
target.node_tree.nodes.clear() target.node_tree.nodes.clear()
loader.load(target, data)
# Load nodes # Load nodes
for node in data["node_tree"]["nodes"]: for node in data["node_tree"]["nodes"]:
load_node(data["node_tree"]["nodes"][node], target.node_tree) load_node(data["node_tree"]["nodes"][node], target.node_tree)
@ -205,59 +234,71 @@ class BlMaterial(BlDatablock):
assert(instance) assert(instance)
mat_dumper = Dumper() mat_dumper = Dumper()
mat_dumper.depth = 2 mat_dumper.depth = 2
mat_dumper.exclude_filter = [ mat_dumper.include_filter = [
"is_embed_data", 'name',
"is_evaluated", 'blend_method',
"name_full", 'shadow_method',
"bl_description", 'alpha_threshold',
"bl_icon", 'show_transparent_back',
"bl_idname", 'use_backface_culling',
"bl_label", 'use_screen_refraction',
"preview", 'use_sss_translucency',
"original", 'refraction_depth',
"uuid", 'preview_render_type',
"users", 'use_preview_world',
"alpha_threshold", 'pass_index',
"line_color", 'use_nodes',
"view_center", 'diffuse_color',
'specular_color',
'roughness',
'specular_intensity',
'metallic',
'line_color',
'line_priority',
'is_grease_pencil'
] ]
data = mat_dumper.dump(instance) data = mat_dumper.dump(instance)
if instance.use_nodes:
nodes = {}
for node in instance.node_tree.nodes:
nodes[node.name] = dump_node(node)
data["node_tree"]['nodes'] = nodes
data["node_tree"]["links"] = dump_links(instance.node_tree.links)
if instance.is_grease_pencil: if instance.is_grease_pencil:
gp_mat_dumper = Dumper() gp_mat_dumper = Dumper()
gp_mat_dumper.depth = 3 gp_mat_dumper.depth = 3
gp_mat_dumper.include_filter = [ gp_mat_dumper.include_filter = [
'color',
'fill_color',
'mix_color',
'mix_factor',
'mix_stroke_factor',
# 'texture_angle',
# 'texture_scale',
# 'texture_offset',
'pixel_size',
'hide',
'lock',
'ghost',
# 'texture_clamp',
'flip',
'use_overlap_strokes',
'show_stroke', 'show_stroke',
'show_fill',
'alignment_mode',
'pass_index',
'mode', 'mode',
'stroke_style', 'stroke_style',
'color', # 'stroke_image',
'use_overlap_strokes',
'show_fill',
'fill_style', 'fill_style',
'fill_color',
'pass_index',
'alignment_mode',
# 'fill_image',
'texture_opacity',
'mix_factor',
'texture_offset',
'texture_angle',
'texture_scale',
'texture_clamp',
'gradient_type', 'gradient_type',
'mix_color', # 'fill_image',
'flip'
] ]
data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil) data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil)
elif instance.use_nodes:
nodes = {}
data["node_tree"] = {}
for node in instance.node_tree.nodes:
nodes[node.name] = dump_node(node)
data["node_tree"]['nodes'] = nodes
data["node_tree"]["links"] = dump_links(instance.node_tree.links)
return data return data
def _resolve_deps_implementation(self): def _resolve_deps_implementation(self):
@ -265,9 +306,7 @@ class BlMaterial(BlDatablock):
deps = [] deps = []
if self.instance.use_nodes: if self.instance.use_nodes:
for node in self.instance.node_tree.nodes: deps.extend(get_node_tree_dependencies(self.instance.node_tree))
if node.type in ['TEX_IMAGE','TEX_ENVIRONMENT']:
deps.append(node.image)
if self.is_library: if self.is_library:
deps.append(self.instance.library) deps.append(self.instance.library)

View File

@ -25,7 +25,7 @@ import numpy as np
from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection
from replication.constants import DIFF_BINARY from replication.constants import DIFF_BINARY
from replication.exception import ContextError from replication.exception import ContextError
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock, get_datablock_from_uuid
VERTICE = ['co'] VERTICE = ['co']
@ -70,8 +70,17 @@ class BlMesh(BlDatablock):
# MATERIAL SLOTS # MATERIAL SLOTS
target.materials.clear() target.materials.clear()
for m in data["material_list"]: for mat_uuid, mat_name in data["material_list"]:
target.materials.append(bpy.data.materials[m]) mat_ref = None
if mat_uuid is not None:
mat_ref = get_datablock_from_uuid(mat_uuid, None)
else:
mat_ref = bpy.data.materials.get(mat_name, None)
if mat_ref is None:
raise Exception("Material doesn't exist")
target.materials.append(mat_ref)
# CLEAR GEOMETRY # CLEAR GEOMETRY
if target.vertices: if target.vertices:
@ -166,7 +175,7 @@ class BlMesh(BlDatablock):
m_list = [] m_list = []
for material in instance.materials: for material in instance.materials:
if material: if material:
m_list.append(material.name) m_list.append((material.uuid,material.name))
data['material_list'] = m_list data['material_list'] = m_list

View File

@ -26,6 +26,241 @@ from replication.constants import (DIFF_JSON, MODIFIED)
from deepdiff import DeepDiff from deepdiff import DeepDiff
import logging import logging
RENDER_SETTINGS = [
'dither_intensity',
'engine',
'film_transparent',
'filter_size',
'fps',
'fps_base',
'frame_map_new',
'frame_map_old',
'hair_subdiv',
'hair_type',
'line_thickness',
'line_thickness_mode',
'metadata_input',
'motion_blur_shutter',
'pixel_aspect_x',
'pixel_aspect_y',
'preview_pixel_size',
'preview_start_resolution',
'resolution_percentage',
'resolution_x',
'resolution_y',
'sequencer_gl_preview',
'use_bake_clear',
'use_bake_lores_mesh',
'use_bake_multires',
'use_bake_selected_to_active',
'use_bake_user_scale',
'use_border',
'use_compositing',
'use_crop_to_border',
'use_file_extension',
'use_freestyle',
'use_full_sample',
'use_high_quality_normals',
'use_lock_interface',
'use_motion_blur',
'use_multiview',
'use_sequencer',
'use_sequencer_override_scene_strip',
'use_single_layer',
'views_format',
]
EVEE_SETTINGS = [
'gi_diffuse_bounces',
'gi_cubemap_resolution',
'gi_visibility_resolution',
'gi_irradiance_smoothing',
'gi_glossy_clamp',
'gi_filter_quality',
'gi_show_irradiance',
'gi_show_cubemaps',
'gi_irradiance_display_size',
'gi_cubemap_display_size',
'gi_auto_bake',
'taa_samples',
'taa_render_samples',
'use_taa_reprojection',
'sss_samples',
'sss_jitter_threshold',
'use_ssr',
'use_ssr_refraction',
'use_ssr_halfres',
'ssr_quality',
'ssr_max_roughness',
'ssr_thickness',
'ssr_border_fade',
'ssr_firefly_fac',
'volumetric_start',
'volumetric_end',
'volumetric_tile_size',
'volumetric_samples',
'volumetric_sample_distribution',
'use_volumetric_lights',
'volumetric_light_clamp',
'use_volumetric_shadows',
'volumetric_shadow_samples',
'use_gtao',
'use_gtao_bent_normals',
'use_gtao_bounce',
'gtao_factor',
'gtao_quality',
'gtao_distance',
'bokeh_max_size',
'bokeh_threshold',
'use_bloom',
'bloom_threshold',
'bloom_color',
'bloom_knee',
'bloom_radius',
'bloom_clamp',
'bloom_intensity',
'use_motion_blur',
'motion_blur_shutter',
'motion_blur_depth_scale',
'motion_blur_max',
'motion_blur_steps',
'shadow_cube_size',
'shadow_cascade_size',
'use_shadow_high_bitdepth',
'gi_diffuse_bounces',
'gi_cubemap_resolution',
'gi_visibility_resolution',
'gi_irradiance_smoothing',
'gi_glossy_clamp',
'gi_filter_quality',
'gi_show_irradiance',
'gi_show_cubemaps',
'gi_irradiance_display_size',
'gi_cubemap_display_size',
'gi_auto_bake',
'taa_samples',
'taa_render_samples',
'use_taa_reprojection',
'sss_samples',
'sss_jitter_threshold',
'use_ssr',
'use_ssr_refraction',
'use_ssr_halfres',
'ssr_quality',
'ssr_max_roughness',
'ssr_thickness',
'ssr_border_fade',
'ssr_firefly_fac',
'volumetric_start',
'volumetric_end',
'volumetric_tile_size',
'volumetric_samples',
'volumetric_sample_distribution',
'use_volumetric_lights',
'volumetric_light_clamp',
'use_volumetric_shadows',
'volumetric_shadow_samples',
'use_gtao',
'use_gtao_bent_normals',
'use_gtao_bounce',
'gtao_factor',
'gtao_quality',
'gtao_distance',
'bokeh_max_size',
'bokeh_threshold',
'use_bloom',
'bloom_threshold',
'bloom_color',
'bloom_knee',
'bloom_radius',
'bloom_clamp',
'bloom_intensity',
'use_motion_blur',
'motion_blur_shutter',
'motion_blur_depth_scale',
'motion_blur_max',
'motion_blur_steps',
'shadow_cube_size',
'shadow_cascade_size',
'use_shadow_high_bitdepth',
]
CYCLES_SETTINGS = [
'shading_system',
'progressive',
'use_denoising',
'denoiser',
'use_square_samples',
'samples',
'aa_samples',
'diffuse_samples',
'glossy_samples',
'transmission_samples',
'ao_samples',
'mesh_light_samples',
'subsurface_samples',
'volume_samples',
'sampling_pattern',
'use_layer_samples',
'sample_all_lights_direct',
'sample_all_lights_indirect',
'light_sampling_threshold',
'use_adaptive_sampling',
'adaptive_threshold',
'adaptive_min_samples',
'min_light_bounces',
'min_transparent_bounces',
'caustics_reflective',
'caustics_refractive',
'blur_glossy',
'max_bounces',
'diffuse_bounces',
'glossy_bounces',
'transmission_bounces',
'volume_bounces',
'transparent_max_bounces',
'volume_step_rate',
'volume_max_steps',
'dicing_rate',
'max_subdivisions',
'dicing_camera',
'offscreen_dicing_scale',
'film_exposure',
'film_transparent_glass',
'film_transparent_roughness',
'filter_type',
'pixel_filter_type',
'filter_width',
'seed',
'use_animated_seed',
'sample_clamp_direct',
'sample_clamp_indirect',
'tile_order',
'use_progressive_refine',
'bake_type',
'use_camera_cull',
'camera_cull_margin',
'use_distance_cull',
'distance_cull_margin',
'motion_blur_position',
'rolling_shutter_type',
'rolling_shutter_duration',
'texture_limit',
'texture_limit_render',
'ao_bounces',
'ao_bounces_render',
]
VIEW_SETTINGS = [
'look',
'view_transform',
'exposure',
'gamma',
'use_curve_mapping',
'white_level',
'black_level'
]
class BlScene(BlDatablock): class BlScene(BlDatablock):
bl_id = "scenes" bl_id = "scenes"
bl_class = bpy.types.Scene bl_class = bpy.types.Scene
@ -50,12 +285,14 @@ class BlScene(BlDatablock):
loader.load(target, data) loader.load(target, data)
# Load master collection # Load master collection
load_collection_objects(data['collection']['objects'], target.collection) load_collection_objects(
load_collection_childrens(data['collection']['children'], target.collection) data['collection']['objects'], target.collection)
load_collection_childrens(
data['collection']['children'], target.collection)
if 'world' in data.keys(): if 'world' in data.keys():
target.world = bpy.data.worlds[data['world']] target.world = bpy.data.worlds[data['world']]
# Annotation # Annotation
if 'grease_pencil' in data.keys(): if 'grease_pencil' in data.keys():
target.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']] target.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']]
@ -65,17 +302,20 @@ class BlScene(BlDatablock):
loader.load(target.eevee, data['eevee']) loader.load(target.eevee, data['eevee'])
if 'cycles' in data.keys(): if 'cycles' in data.keys():
loader.load(target.eevee, data['cycles']) loader.load(target.cycles, data['cycles'])
if 'render' in data.keys(): if 'render' in data.keys():
loader.load(target.render, data['render']) loader.load(target.render, data['render'])
if 'view_settings' in data.keys(): if 'view_settings' in data.keys():
loader.load(target.view_settings, data['view_settings']) loader.load(target.view_settings, data['view_settings'])
if target.view_settings.use_curve_mapping: if target.view_settings.use_curve_mapping and \
#TODO: change this ugly fix 'curve_mapping' in data['view_settings']:
target.view_settings.curve_mapping.white_level = data['view_settings']['curve_mapping']['white_level'] # TODO: change this ugly fix
target.view_settings.curve_mapping.black_level = data['view_settings']['curve_mapping']['black_level'] target.view_settings.curve_mapping.white_level = data[
'view_settings']['curve_mapping']['white_level']
target.view_settings.curve_mapping.black_level = data[
'view_settings']['curve_mapping']['black_level']
target.view_settings.curve_mapping.update() target.view_settings.curve_mapping.update()
def _dump_implementation(self, data, instance=None): def _dump_implementation(self, data, instance=None):
@ -100,48 +340,43 @@ class BlScene(BlDatablock):
scene_dumper.depth = 3 scene_dumper.depth = 3
scene_dumper.include_filter = ['children','objects','name'] scene_dumper.include_filter = ['children', 'objects', 'name']
data['collection'] = {} data['collection'] = {}
data['collection']['children'] = dump_collection_children(instance.collection) data['collection']['children'] = dump_collection_children(
data['collection']['objects'] = dump_collection_objects(instance.collection) instance.collection)
data['collection']['objects'] = dump_collection_objects(
instance.collection)
scene_dumper.depth = 1 scene_dumper.depth = 1
scene_dumper.include_filter = None scene_dumper.include_filter = None
if self.preferences.sync_flags.sync_render_settings: if self.preferences.sync_flags.sync_render_settings:
scene_dumper.exclude_filter = [ scene_dumper.include_filter = RENDER_SETTINGS
'gi_cache_info',
'feature_set',
'debug_use_hair_bvh',
'aa_samples',
'blur_glossy',
'glossy_bounces',
'device',
'max_bounces',
'preview_aa_samples',
'preview_samples',
'sample_clamp_indirect',
'samples',
'volume_bounces',
'file_extension',
'use_denoising'
]
data['eevee'] = scene_dumper.dump(instance.eevee)
data['cycles'] = scene_dumper.dump(instance.cycles)
data['view_settings'] = scene_dumper.dump(instance.view_settings)
data['render'] = scene_dumper.dump(instance.render) data['render'] = scene_dumper.dump(instance.render)
if instance.render.engine == 'BLENDER_EEVEE':
scene_dumper.include_filter = EVEE_SETTINGS
data['eevee'] = scene_dumper.dump(instance.eevee)
elif instance.render.engine == 'CYCLES':
scene_dumper.include_filter = CYCLES_SETTINGS
data['cycles'] = scene_dumper.dump(instance.cycles)
scene_dumper.include_filter = VIEW_SETTINGS
data['view_settings'] = scene_dumper.dump(instance.view_settings)
if instance.view_settings.use_curve_mapping: if instance.view_settings.use_curve_mapping:
data['view_settings']['curve_mapping'] = scene_dumper.dump(instance.view_settings.curve_mapping) data['view_settings']['curve_mapping'] = scene_dumper.dump(
instance.view_settings.curve_mapping)
scene_dumper.depth = 5 scene_dumper.depth = 5
scene_dumper.include_filter = [ scene_dumper.include_filter = [
'curves', 'curves',
'points', 'points',
'location' 'location',
] ]
data['view_settings']['curve_mapping']['curves'] = scene_dumper.dump(instance.view_settings.curve_mapping.curves) data['view_settings']['curve_mapping']['curves'] = scene_dumper.dump(
instance.view_settings.curve_mapping.curves)
return data return data
def _resolve_deps_implementation(self): def _resolve_deps_implementation(self):
@ -150,15 +385,15 @@ class BlScene(BlDatablock):
# child collections # child collections
for child in self.instance.collection.children: for child in self.instance.collection.children:
deps.append(child) deps.append(child)
# childs objects # childs objects
for object in self.instance.objects: for object in self.instance.collection.objects:
deps.append(object) deps.append(object)
# world # world
if self.instance.world: if self.instance.world:
deps.append(self.instance.world) deps.append(self.instance.world)
# annotations # annotations
if self.instance.grease_pencil: if self.instance.grease_pencil:
deps.append(self.instance.grease_pencil) deps.append(self.instance.grease_pencil)
@ -177,4 +412,4 @@ class BlScene(BlDatablock):
if not self.preferences.sync_flags.sync_active_camera: if not self.preferences.sync_flags.sync_active_camera:
exclude_path.append("root['camera']") exclude_path.append("root['camera']")
return DeepDiff(self.data, self._dump(instance=self.instance),exclude_paths=exclude_path, cache_size=5000) return DeepDiff(self.data, self._dump(instance=self.instance), exclude_paths=exclude_path)

View File

@ -21,7 +21,11 @@ import mathutils
from .dump_anything import Loader, Dumper from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock from .bl_datablock import BlDatablock
from .bl_material import load_links, load_node, dump_node, dump_links from .bl_material import (load_links,
load_node,
dump_node,
dump_links,
get_node_tree_dependencies)
class BlWorld(BlDatablock): class BlWorld(BlDatablock):
@ -39,7 +43,7 @@ class BlWorld(BlDatablock):
def _load_implementation(self, data, target): def _load_implementation(self, data, target):
loader = Loader() loader = Loader()
loader.load(target, data) loader.load(target, data)
if data["use_nodes"]: if data["use_nodes"]:
if target.node_tree is None: if target.node_tree is None:
target.use_nodes = True target.use_nodes = True
@ -52,7 +56,6 @@ class BlWorld(BlDatablock):
# Load nodes links # Load nodes links
target.node_tree.links.clear() target.node_tree.links.clear()
load_links(data["node_tree"]["links"], target.node_tree) load_links(data["node_tree"]["links"], target.node_tree)
def _dump_implementation(self, data, instance=None): def _dump_implementation(self, data, instance=None):
@ -83,10 +86,7 @@ class BlWorld(BlDatablock):
deps = [] deps = []
if self.instance.use_nodes: if self.instance.use_nodes:
for node in self.instance.node_tree.nodes: deps.extend(get_node_tree_dependencies(self.instance.node_tree))
if node.type in ['TEX_IMAGE','TEX_ENVIRONMENT']:
deps.append(node.image)
if self.is_library: if self.is_library:
deps.append(self.instance.library) deps.append(self.instance.library)
return deps return deps

View File

@ -24,8 +24,8 @@ import numpy as np
BPY_TO_NUMPY_TYPES = { BPY_TO_NUMPY_TYPES = {
'FLOAT': np.float, 'FLOAT': np.float32,
'INT': np.int, 'INT': np.int32,
'BOOL': np.bool} 'BOOL': np.bool}
PRIMITIVE_TYPES = ['FLOAT', 'INT', 'BOOLEAN'] PRIMITIVE_TYPES = ['FLOAT', 'INT', 'BOOLEAN']
@ -47,7 +47,7 @@ def np_load_collection(dikt: dict, collection: bpy.types.CollectionProperty, att
:type attributes: list :type attributes: list
""" """
if not dikt or len(collection) == 0: if not dikt or len(collection) == 0:
logging.warning(f'Skipping collection') logging.debug(f'Skipping collection {collection}')
return return
if attributes is None: if attributes is None:
@ -626,11 +626,11 @@ class Loader:
for k in self._ordered_keys(dump.keys()): for k in self._ordered_keys(dump.keys()):
v = dump[k] v = dump[k]
if not hasattr(default.read(), k): if not hasattr(default.read(), k):
logging.debug(f"Load default, skipping {default} : {k}") continue
try: try:
self._load_any(default.extend(k), v) self._load_any(default.extend(k), v)
except Exception as err: except Exception as err:
logging.debug(f"Cannot load {k}: {err}") logging.debug(f"Skipping {k}")
@property @property
def match_subset_all(self): def match_subset_all(self):

View File

@ -19,7 +19,15 @@ import logging
import bpy import bpy
from . import presence, utils from . import utils
from .presence import (renderer,
UserFrustumWidget,
UserNameWidget,
UserSelectionWidget,
refresh_3d_view,
generate_user_camera,
get_view_matrix,
refresh_sidebar_view)
from replication.constants import (FETCHED, from replication.constants import (FETCHED,
UP, UP,
RP_COMMON, RP_COMMON,
@ -32,10 +40,12 @@ from replication.constants import (FETCHED,
REPARENT) REPARENT)
from replication.interface import session from replication.interface import session
from replication.exception import NonAuthorizedOperationError
class Delayable(): class Delayable():
"""Delayable task interface """Delayable task interface
""" """
def __init__(self): def __init__(self):
self.is_registered = False self.is_registered = False
@ -63,13 +73,14 @@ class Timer(Delayable):
def register(self): def register(self):
"""Register the timer into the blender timer system """Register the timer into the blender timer system
""" """
if not self.is_registered: if not self.is_registered:
bpy.app.timers.register(self.main) bpy.app.timers.register(self.main)
self.is_registered = True self.is_registered = True
logging.debug(f"Register {self.__class__.__name__}") logging.debug(f"Register {self.__class__.__name__}")
else: else:
logging.debug(f"Timer {self.__class__.__name__} already registered") logging.debug(
f"Timer {self.__class__.__name__} already registered")
def main(self): def main(self):
self.execute() self.execute()
@ -108,14 +119,14 @@ class ApplyTimer(Timer):
if node_ref.state == FETCHED: if node_ref.state == FETCHED:
try: try:
session.apply(node, force=True) session.apply(node)
except Exception as e: except Exception as e:
logging.error(f"Fail to apply {node_ref.uuid}: {e}") logging.error(f"Fail to apply {node_ref.uuid}: {e}")
elif node_ref.state == REPARENT: elif node_ref.state == REPARENT:
# Reload the node # Reload the node
node_ref.remove_instance() node_ref.remove_instance()
node_ref.resolve() node_ref.resolve()
session.apply(node, force=True) session.apply(node)
for parent in session._graph.find_parents(node): for parent in session._graph.find_parents(node):
logging.info(f"Applying parent {parent}") logging.info(f"Applying parent {parent}")
session.apply(parent, force=True) session.apply(parent, force=True)
@ -156,10 +167,14 @@ class DynamicRightSelectTimer(Timer):
recursive = True recursive = True
if node.data and 'instance_type' in node.data.keys(): if node.data and 'instance_type' in node.data.keys():
recursive = node.data['instance_type'] != 'COLLECTION' recursive = node.data['instance_type'] != 'COLLECTION'
session.change_owner( try:
node.uuid, session.change_owner(
RP_COMMON, node.uuid,
recursive=recursive) RP_COMMON,
ignore_warnings=True,
affect_dependencies=recursive)
except NonAuthorizedOperationError:
logging.warning(f"Not authorized to change {node} owner")
# change new selection to our # change new selection to our
for obj in obj_ours: for obj in obj_ours:
@ -170,10 +185,14 @@ class DynamicRightSelectTimer(Timer):
if node.data and 'instance_type' in node.data.keys(): if node.data and 'instance_type' in node.data.keys():
recursive = node.data['instance_type'] != 'COLLECTION' recursive = node.data['instance_type'] != 'COLLECTION'
session.change_owner( try:
node.uuid, session.change_owner(
settings.username, node.uuid,
recursive=recursive) settings.username,
ignore_warnings=True,
affect_dependencies=recursive)
except NonAuthorizedOperationError:
logging.warning(f"Not authorized to change {node} owner")
else: else:
return return
@ -192,77 +211,21 @@ class DynamicRightSelectTimer(Timer):
filter_owner=settings.username) filter_owner=settings.username)
for key in owned_keys: for key in owned_keys:
node = session.get(uuid=key) node = session.get(uuid=key)
try:
session.change_owner(
key,
RP_COMMON,
ignore_warnings=True,
affect_dependencies=recursive)
except NonAuthorizedOperationError:
logging.warning(f"Not authorized to change {key} owner")
session.change_owner( for obj in bpy.data.objects:
key, object_uuid = getattr(obj, 'uuid', None)
RP_COMMON, if object_uuid:
recursive=recursive) is_selectable = not session.is_readonly(object_uuid)
if obj.hide_select != is_selectable:
for user, user_info in session.online_users.items(): obj.hide_select = is_selectable
if user != settings.username:
metadata = user_info.get('metadata')
if 'selected_objects' in metadata:
# Update selectionnable objects
for obj in bpy.data.objects:
if obj.hide_select and obj.uuid not in metadata['selected_objects']:
obj.hide_select = False
elif not obj.hide_select and obj.uuid in metadata['selected_objects']:
obj.hide_select = True
class Draw(Delayable):
def __init__(self):
super().__init__()
self._handler = None
def register(self):
if not self.is_registered:
self._handler = bpy.types.SpaceView3D.draw_handler_add(
self.execute, (), 'WINDOW', 'POST_VIEW')
logging.debug(f"Register {self.__class__.__name__}")
else:
logging.debug(f"Drow {self.__class__.__name__} already registered")
def execute(self):
raise NotImplementedError()
def unregister(self):
try:
bpy.types.SpaceView3D.draw_handler_remove(
self._handler, "WINDOW")
except:
pass
class DrawClient(Draw):
def execute(self):
renderer = getattr(presence, 'renderer', None)
prefs = utils.get_preferences()
if session and renderer and session.state['STATE'] == STATE_ACTIVE:
settings = bpy.context.window_manager.session
users = session.online_users
# Update users
for user in users.values():
metadata = user.get('metadata')
color = metadata.get('color')
scene_current = metadata.get('scene_current')
user_showable = scene_current == bpy.context.scene.name or settings.presence_show_far_user
if color and scene_current and user_showable:
if settings.presence_show_selected and 'selected_objects' in metadata.keys():
renderer.draw_client_selection(
user['id'], color, metadata['selected_objects'])
if settings.presence_show_user and 'view_corners' in metadata:
renderer.draw_client_camera(
user['id'], metadata['view_corners'], color)
if not user_showable:
# TODO: remove this when user event drivent update will be
# ready
renderer.flush_selection()
renderer.flush_users()
class ClientUpdate(Timer): class ClientUpdate(Timer):
def __init__(self, timout=.1): def __init__(self, timout=.1):
@ -272,7 +235,6 @@ class ClientUpdate(Timer):
def execute(self): def execute(self):
settings = utils.get_preferences() settings = utils.get_preferences()
renderer = getattr(presence, 'renderer', None)
if session and renderer: if session and renderer:
if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]: if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]:
@ -291,7 +253,7 @@ class ClientUpdate(Timer):
if cached_user_data is None: if cached_user_data is None:
self.users_metadata[username] = user_data['metadata'] self.users_metadata[username] = user_data['metadata']
elif 'view_matrix' in cached_user_data and 'view_matrix' in new_user_data and cached_user_data['view_matrix'] != new_user_data['view_matrix']: elif 'view_matrix' in cached_user_data and 'view_matrix' in new_user_data and cached_user_data['view_matrix'] != new_user_data['view_matrix']:
presence.refresh_3d_view() refresh_3d_view()
self.users_metadata[username] = user_data['metadata'] self.users_metadata[username] = user_data['metadata']
break break
else: else:
@ -300,13 +262,13 @@ class ClientUpdate(Timer):
local_user_metadata = local_user.get('metadata') local_user_metadata = local_user.get('metadata')
scene_current = bpy.context.scene.name scene_current = bpy.context.scene.name
local_user = session.online_users.get(settings.username) local_user = session.online_users.get(settings.username)
current_view_corners = presence.get_view_corners() current_view_corners = generate_user_camera()
# Init client metadata # Init client metadata
if not local_user_metadata or 'color' not in local_user_metadata.keys(): if not local_user_metadata or 'color' not in local_user_metadata.keys():
metadata = { metadata = {
'view_corners': presence.get_view_matrix(), 'view_corners': get_view_matrix(),
'view_matrix': presence.get_view_matrix(), 'view_matrix': get_view_matrix(),
'color': (settings.client_color.r, 'color': (settings.client_color.r,
settings.client_color.g, settings.client_color.g,
settings.client_color.b, settings.client_color.b,
@ -323,7 +285,7 @@ class ClientUpdate(Timer):
session.update_user_metadata(local_user_metadata) session.update_user_metadata(local_user_metadata)
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']: elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']:
local_user_metadata['view_corners'] = current_view_corners local_user_metadata['view_corners'] = current_view_corners
local_user_metadata['view_matrix'] = presence.get_view_matrix( local_user_metadata['view_matrix'] = get_view_matrix(
) )
session.update_user_metadata(local_user_metadata) session.update_user_metadata(local_user_metadata)
@ -333,26 +295,27 @@ class SessionStatusUpdate(Timer):
super().__init__(timout) super().__init__(timout)
def execute(self): def execute(self):
presence.refresh_sidebar_view() refresh_sidebar_view()
class SessionUserSync(Timer): class SessionUserSync(Timer):
def __init__(self, timout=1): def __init__(self, timout=1):
super().__init__(timout) super().__init__(timout)
self.settings = utils.get_preferences()
def execute(self): def execute(self):
renderer = getattr(presence, 'renderer', None)
if session and renderer: if session and renderer:
# sync online users # sync online users
session_users = session.online_users session_users = session.online_users
ui_users = bpy.context.window_manager.online_users ui_users = bpy.context.window_manager.online_users
for index, user in enumerate(ui_users): for index, user in enumerate(ui_users):
if user.username not in session_users.keys(): if user.username not in session_users.keys() and \
user.username != self.settings.username:
renderer.remove_widget(f"{user.username}_cam")
renderer.remove_widget(f"{user.username}_select")
renderer.remove_widget(f"{user.username}_name")
ui_users.remove(index) ui_users.remove(index)
renderer.flush_selection()
renderer.flush_users()
break break
for user in session_users: for user in session_users:
@ -360,13 +323,20 @@ class SessionUserSync(Timer):
new_key = ui_users.add() new_key = ui_users.add()
new_key.name = user new_key.name = user
new_key.username = user new_key.username = user
if user != self.settings.username:
renderer.add_widget(
f"{user}_cam", UserFrustumWidget(user))
renderer.add_widget(
f"{user}_select", UserSelectionWidget(user))
renderer.add_widget(
f"{user}_name", UserNameWidget(user))
class MainThreadExecutor(Timer): class MainThreadExecutor(Timer):
def __init__(self, timout=1, execution_queue=None): def __init__(self, timout=1, execution_queue=None):
super().__init__(timout) super().__init__(timout)
self.execution_queue = execution_queue self.execution_queue = execution_queue
def execute(self): def execute(self):
while not self.execution_queue.empty(): while not self.execution_queue.empty():
function = self.execution_queue.get() function = self.execution_queue.get()

View File

@ -21,26 +21,24 @@ import logging
import os import os
import queue import queue
import random import random
import shutil
import string import string
import time import time
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
import shutil
from pathlib import Path
from queue import Queue from queue import Queue
import bpy import bpy
import mathutils import mathutils
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
from . import bl_types, delayable, environment, presence, ui, utils STATE_INITIAL, STATE_SYNCING, UP)
from replication.constants import (FETCHED, STATE_ACTIVE,
STATE_INITIAL,
STATE_SYNCING, RP_COMMON, UP)
from replication.data import ReplicatedDataFactory from replication.data import ReplicatedDataFactory
from replication.exception import NonAuthorizedOperationError from replication.exception import NonAuthorizedOperationError
from replication.interface import session from replication.interface import session
from . import bl_types, delayable, environment, ui, utils
from .presence import (SessionStatusWidget, renderer, view3d_find)
background_execution_queue = Queue() background_execution_queue = Queue()
delayables = [] delayables = []
@ -80,10 +78,6 @@ def initialize_session():
if node_ref.state == FETCHED: if node_ref.state == FETCHED:
node_ref.apply() node_ref.apply()
# Step 3: Launch presence overlay
if runtime_settings.enable_presence:
presence.renderer.run()
# Step 4: Register blender timers # Step 4: Register blender timers
for d in delayables: for d in delayables:
d.register() d.register()
@ -91,6 +85,8 @@ def initialize_session():
if settings.update_method == 'DEPSGRAPH': if settings.update_method == 'DEPSGRAPH':
bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation) bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
bpy.ops.session.apply_armature_operator('INVOKE_DEFAULT')
@session_callback('on_exit') @session_callback('on_exit')
def on_connection_end(): def on_connection_end():
@ -108,9 +104,6 @@ def on_connection_end():
stop_modal_executor = True stop_modal_executor = True
# Step 2: Unregister presence renderer
presence.renderer.stop()
if settings.update_method == 'DEPSGRAPH': if settings.update_method == 'DEPSGRAPH':
bpy.app.handlers.depsgraph_update_post.remove( bpy.app.handlers.depsgraph_update_post.remove(
depsgraph_evaluation) depsgraph_evaluation)
@ -233,7 +226,8 @@ class SessionStartOperator(bpy.types.Operator):
except Exception as e: except Exception as e:
self.report({'ERROR'}, repr(e)) self.report({'ERROR'}, repr(e))
logging.error(f"Error: {e}") logging.error(f"Error: {e}")
import traceback
traceback.print_exc()
# Join a session # Join a session
else: else:
if not runtime_settings.admin: if not runtime_settings.admin:
@ -256,7 +250,6 @@ class SessionStartOperator(bpy.types.Operator):
# Background client updates service # Background client updates service
delayables.append(delayable.ClientUpdate()) delayables.append(delayable.ClientUpdate())
delayables.append(delayable.DrawClient())
delayables.append(delayable.DynamicRightSelectTimer()) delayables.append(delayable.DynamicRightSelectTimer())
session_update = delayable.SessionStatusUpdate() session_update = delayable.SessionStatusUpdate()
@ -272,7 +265,7 @@ class SessionStartOperator(bpy.types.Operator):
delayables.append(session_update) delayables.append(session_update)
delayables.append(session_user_sync) delayables.append(session_user_sync)
bpy.ops.session.apply_armature_operator()
self.report( self.report(
{'INFO'}, {'INFO'},
@ -347,7 +340,7 @@ class SessionStopOperator(bpy.types.Operator):
class SessionKickOperator(bpy.types.Operator): class SessionKickOperator(bpy.types.Operator):
bl_idname = "session.kick" bl_idname = "session.kick"
bl_label = "Kick" bl_label = "Kick"
bl_description = "Kick the user" bl_description = "Kick the target user"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
user: bpy.props.StringProperty() user: bpy.props.StringProperty()
@ -377,8 +370,9 @@ class SessionKickOperator(bpy.types.Operator):
class SessionPropertyRemoveOperator(bpy.types.Operator): class SessionPropertyRemoveOperator(bpy.types.Operator):
bl_idname = "session.remove_prop" bl_idname = "session.remove_prop"
bl_label = "remove" bl_label = "Delete cache"
bl_description = "broadcast a property to connected client_instances" bl_description = "Stop tracking modification on the target datablock." + \
"The datablock will no longer be updated for others client. "
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
property_path: bpy.props.StringProperty(default="None") property_path: bpy.props.StringProperty(default="None")
@ -401,11 +395,12 @@ class SessionPropertyRemoveOperator(bpy.types.Operator):
class SessionPropertyRightOperator(bpy.types.Operator): class SessionPropertyRightOperator(bpy.types.Operator):
bl_idname = "session.right" bl_idname = "session.right"
bl_label = "Change owner to" bl_label = "Change modification rights"
bl_description = "Change owner of specified datablock" bl_description = "Modify the owner of the target datablock"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
key: bpy.props.StringProperty(default="None") key: bpy.props.StringProperty(default="None")
recursive: bpy.props.BoolProperty(default=True)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -419,14 +414,21 @@ class SessionPropertyRightOperator(bpy.types.Operator):
layout = self.layout layout = self.layout
runtime_settings = context.window_manager.session runtime_settings = context.window_manager.session
col = layout.column() row = layout.row()
col.prop(runtime_settings, "clients") row.label(text="Give the owning rights to:")
row.prop(runtime_settings, "clients", text="")
row = layout.row()
row.label(text="Affect dependencies")
row.prop(self, "recursive", text="")
def execute(self, context): def execute(self, context):
runtime_settings = context.window_manager.session runtime_settings = context.window_manager.session
if session: if session:
session.change_owner(self.key, runtime_settings.clients) session.change_owner(self.key,
runtime_settings.clients,
ignore_warnings=True,
affect_dependencies=self.recursive)
return {"FINISHED"} return {"FINISHED"}
@ -472,7 +474,7 @@ class SessionSnapUserOperator(bpy.types.Operator):
return {'CANCELLED'} return {'CANCELLED'}
if event.type == 'TIMER': if event.type == 'TIMER':
area, region, rv3d = presence.view3d_find() area, region, rv3d = view3d_find()
if session: if session:
target_ref = session.online_users.get(self.target_client) target_ref = session.online_users.get(self.target_client)
@ -559,26 +561,31 @@ class SessionSnapTimeOperator(bpy.types.Operator):
class SessionApply(bpy.types.Operator): class SessionApply(bpy.types.Operator):
bl_idname = "session.apply" bl_idname = "session.apply"
bl_label = "apply selected block into blender" bl_label = "Revert"
bl_description = "Apply selected block into blender" bl_description = "Revert the selected datablock from his cached" + \
" version."
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
target: bpy.props.StringProperty() target: bpy.props.StringProperty()
reset_dependencies: bpy.props.BoolProperty(default=False)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return True return True
def execute(self, context): def execute(self, context):
session.apply(self.target) logging.debug(f"Running apply on {self.target}")
session.apply(self.target,
force=True,
force_dependencies=self.reset_dependencies)
return {"FINISHED"} return {"FINISHED"}
class SessionCommit(bpy.types.Operator): class SessionCommit(bpy.types.Operator):
bl_idname = "session.commit" bl_idname = "session.commit"
bl_label = "commit and push selected datablock to server" bl_label = "Force server update"
bl_description = "commit and push selected datablock to server" bl_description = "Commit and push the target datablock to server"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
target: bpy.props.StringProperty() target: bpy.props.StringProperty()
@ -690,10 +697,12 @@ def sanitize_deps_graph(dummy):
A future solution should be to avoid storing dataclock reference... A future solution should be to avoid storing dataclock reference...
""" """
if session and session.state['STATE'] == STATE_ACTIVE: if session and session.state['STATE'] == STATE_ACTIVE:
for node_key in session.list(): for node_key in session.list():
session.get(node_key).resolve() node = session.get(node_key)
if node and not node.resolve(construct=False):
session.remove(node_key)
@persistent @persistent
def load_pre_handler(dummy): def load_pre_handler(dummy):
@ -746,6 +755,7 @@ def depsgraph_evaluation(scene):
def register(): def register():
from bpy.utils import register_class from bpy.utils import register_class
for cls in classes: for cls in classes:
register_class(cls) register_class(cls)

View File

@ -41,7 +41,7 @@ def randomColor():
def random_string_digits(stringLength=6): def random_string_digits(stringLength=6):
"""Generate a random string of letters and digits """ """Generate a random string of letters and digits"""
lettersAndDigits = string.ascii_letters + string.digits lettersAndDigits = string.ascii_letters + string.digits
return ''.join(random.choices(lettersAndDigits, k=stringLength)) return ''.join(random.choices(lettersAndDigits, k=stringLength))
@ -68,7 +68,7 @@ def update_port(self, context):
if self.ipc_port < max_port and \ if self.ipc_port < max_port and \
self['ipc_port'] >= self.port: self['ipc_port'] >= self.port:
logging.error( logging.error(
"IPC Port in conflic with the port, assigning a random value") "IPC Port in conflict with the port, assigning a random value")
self['ipc_port'] = random.randrange(self.port+4, 10000) self['ipc_port'] = random.randrange(self.port+4, 10000)
@ -103,14 +103,18 @@ class ReplicatedDatablock(bpy.types.PropertyGroup):
def set_sync_render_settings(self, value): def set_sync_render_settings(self, value):
self['sync_render_settings'] = value self['sync_render_settings'] = value
if session and bpy.context.scene.uuid and value: if session and bpy.context.scene.uuid and value:
bpy.ops.session.apply('INVOKE_DEFAULT', target=bpy.context.scene.uuid) bpy.ops.session.apply('INVOKE_DEFAULT',
target=bpy.context.scene.uuid,
reset_dependencies=False)
def set_sync_active_camera(self, value): def set_sync_active_camera(self, value):
self['sync_active_camera'] = value self['sync_active_camera'] = value
if session and bpy.context.scene.uuid and value: if session and bpy.context.scene.uuid and value:
bpy.ops.session.apply('INVOKE_DEFAULT', target=bpy.context.scene.uuid) bpy.ops.session.apply('INVOKE_DEFAULT',
target=bpy.context.scene.uuid,
reset_dependencies=False)
class ReplicationFlags(bpy.types.PropertyGroup): class ReplicationFlags(bpy.types.PropertyGroup):
@ -123,9 +127,10 @@ class ReplicationFlags(bpy.types.PropertyGroup):
sync_render_settings: bpy.props.BoolProperty( sync_render_settings: bpy.props.BoolProperty(
name="Synchronize render settings", name="Synchronize render settings",
description="Synchronize render settings (eevee and cycles only)", description="Synchronize render settings (eevee and cycles only)",
default=True, default=False,
set=set_sync_render_settings, set=set_sync_render_settings,
get=get_sync_render_settings) get=get_sync_render_settings
)
sync_during_editmode: bpy.props.BoolProperty( sync_during_editmode: bpy.props.BoolProperty(
name="Edit mode updates", name="Edit mode updates",
description="Enable objects update in edit mode (! Impact performances !)", description="Enable objects update in edit mode (! Impact performances !)",
@ -169,7 +174,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
) )
ipc_port: bpy.props.IntProperty( ipc_port: bpy.props.IntProperty(
name="ipc_port", name="ipc_port",
description='internal ttl port(only usefull for multiple local instances)', description='internal ttl port(only useful for multiple local instances)',
default=random.randrange(5570, 70000), default=random.randrange(5570, 70000),
update=update_port, update=update_port,
) )
@ -215,7 +220,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
name="Category", name="Category",
description="Preferences Category", description="Preferences Category",
items=[ items=[
('CONFIG', "Configuration", "Configuration about this add-on"), ('CONFIG', "Configuration", "Configuration of this add-on"),
('UPDATE', "Update", "Update this add-on"), ('UPDATE', "Update", "Update this add-on"),
], ],
default='CONFIG' default='CONFIG'
@ -233,6 +238,31 @@ class SessionPrefs(bpy.types.AddonPreferences):
set=set_log_level, set=set_log_level,
get=get_log_level get=get_log_level
) )
presence_hud_scale: bpy.props.FloatProperty(
name="Text scale",
description="Adjust the session widget text scale",
min=7,
max=90,
default=15,
)
presence_hud_hpos: bpy.props.FloatProperty(
name="Horizontal position",
description="Adjust the session widget horizontal position",
min=1,
max=90,
default=3,
step=1,
subtype='PERCENTAGE',
)
presence_hud_vpos: bpy.props.FloatProperty(
name="Vertical position",
description="Adjust the session widget vertical position",
min=1,
max=94,
default=1,
step=1,
subtype='PERCENTAGE',
)
conf_session_identity_expanded: bpy.props.BoolProperty( conf_session_identity_expanded: bpy.props.BoolProperty(
name="Identity", name="Identity",
description="Identity", description="Identity",
@ -334,7 +364,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
# USER INFORMATIONS # USER INFORMATIONS
box = grid.box() box = grid.box()
box.prop( box.prop(
self, "conf_session_identity_expanded", text="User informations", self, "conf_session_identity_expanded", text="User information",
icon=get_expanded_icon(self.conf_session_identity_expanded), icon=get_expanded_icon(self.conf_session_identity_expanded),
emboss=False) emboss=False)
if self.conf_session_identity_expanded: if self.conf_session_identity_expanded:
@ -344,7 +374,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
# NETWORK SETTINGS # NETWORK SETTINGS
box = grid.box() box = grid.box()
box.prop( box.prop(
self, "conf_session_net_expanded", text="Netorking", self, "conf_session_net_expanded", text="Networking",
icon=get_expanded_icon(self.conf_session_net_expanded), icon=get_expanded_icon(self.conf_session_net_expanded),
emboss=False) emboss=False)
@ -407,6 +437,15 @@ class SessionPrefs(bpy.types.AddonPreferences):
emboss=False) emboss=False)
if self.conf_session_ui_expanded: if self.conf_session_ui_expanded:
box.row().prop(self, "panel_category", text="Panel category", expand=True) box.row().prop(self, "panel_category", text="Panel category", expand=True)
row = box.row()
row.label(text="Session widget:")
col = box.column(align=True)
col.prop(self, "presence_hud_scale", expand=True)
col.prop(self, "presence_hud_hpos", expand=True)
col.prop(self, "presence_hud_vpos", expand=True)
if self.category == 'UPDATE': if self.category == 'UPDATE':
from . import addon_updater_ops from . import addon_updater_ops
@ -476,25 +515,26 @@ class SessionProps(bpy.types.PropertyGroup):
name="Presence overlay", name="Presence overlay",
description='Enable overlay drawing module', description='Enable overlay drawing module',
default=True, default=True,
update=presence.update_presence
) )
presence_show_selected: bpy.props.BoolProperty( presence_show_selected: bpy.props.BoolProperty(
name="Show selected objects", name="Show selected objects",
description='Enable selection overlay ', description='Enable selection overlay ',
default=True, default=True,
update=presence.update_overlay_settings
) )
presence_show_user: bpy.props.BoolProperty( presence_show_user: bpy.props.BoolProperty(
name="Show users", name="Show users",
description='Enable user overlay ', description='Enable user overlay ',
default=True, default=True,
update=presence.update_overlay_settings
) )
presence_show_far_user: bpy.props.BoolProperty( presence_show_far_user: bpy.props.BoolProperty(
name="Show users on different scenes", name="Show users on different scenes",
description="Show user on different scenes", description="Show user on different scenes",
default=False, default=False,
update=presence.update_overlay_settings )
presence_show_session_status: bpy.props.BoolProperty(
name="Show session status ",
description="Show session status on the viewport",
default=True,
) )
filter_owned: bpy.props.BoolProperty( filter_owned: bpy.props.BoolProperty(
name="filter_owned", name="filter_owned",

View File

@ -19,6 +19,7 @@
import copy import copy
import logging import logging
import math import math
import sys
import traceback import traceback
import bgl import bgl
@ -28,13 +29,17 @@ import gpu
import mathutils import mathutils
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG,
STATE_INITIAL, STATE_LAUNCHING_SERVICES,
STATE_LOBBY, STATE_QUITTING, STATE_SRV_SYNC,
STATE_SYNCING, STATE_WAITING)
from replication.interface import session
from . import utils from .utils import find_from_attr, get_state_str, get_preferences
renderer = None # Helper functions
def view3d_find() -> tuple:
def view3d_find():
""" Find the first 'VIEW_3D' windows found in areas """ Find the first 'VIEW_3D' windows found in areas
:return: tuple(Area, Region, RegionView3D) :return: tuple(Area, Region, RegionView3D)
@ -56,36 +61,48 @@ def refresh_3d_view():
if area and region and rv3d: if area and region and rv3d:
area.tag_redraw() area.tag_redraw()
def refresh_sidebar_view(): def refresh_sidebar_view():
""" Refresh the blender sidebar """ Refresh the blender viewport sidebar
""" """
area, region, rv3d = view3d_find() area, region, rv3d = view3d_find()
if area: if area:
area.regions[3].tag_redraw() area.regions[3].tag_redraw()
def get_target(region, rv3d, coord):
def project_to_viewport(region: bpy.types.Region, rv3d: bpy.types.RegionView3D, coords: list, distance: float = 1.0) -> list:
""" Compute a projection from 2D to 3D viewport coordinate
:param region: target windows region
:type region: bpy.types.Region
:param rv3d: view 3D
:type rv3d: bpy.types.RegionView3D
:param coords: coordinate to project
:type coords: list
:param distance: distance offset into viewport
:type distance: float
:return: list of coordinates [x,y,z]
"""
target = [0, 0, 0] target = [0, 0, 0]
if coord and region and rv3d: if coords and region and rv3d:
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coords)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coords)
target = ray_origin + view_vector
return [target.x, target.y, target.z]
def get_target_far(region, rv3d, coord, distance):
target = [0, 0, 0]
if coord and region and rv3d:
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
target = ray_origin + view_vector * distance target = ray_origin + view_vector * distance
return [target.x, target.y, target.z] return [target.x, target.y, target.z]
def get_default_bbox(obj, radius):
def bbox_from_obj(obj: bpy.types.Object, radius: float) -> list:
""" Generate a bounding box for a given object by using its world matrix
:param obj: target object
:type obj: bpy.types.Object
:param radius: bounding box radius
:type radius: float
:return: list of 8 points [(x,y,z),...]
"""
coords = [ coords = [
(-radius, -radius, -radius), (+radius, -radius, -radius), (-radius, -radius, -radius), (+radius, -radius, -radius),
(-radius, +radius, -radius), (+radius, +radius, -radius), (-radius, +radius, -radius), (+radius, +radius, -radius),
@ -93,264 +110,384 @@ def get_default_bbox(obj, radius):
(-radius, +radius, +radius), (+radius, +radius, +radius)] (-radius, +radius, +radius), (+radius, +radius, +radius)]
base = obj.matrix_world base = obj.matrix_world
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords] bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
return [(point.x, point.y, point.z) return [(point.x, point.y, point.z)
for point in bbox_corners] for point in bbox_corners]
def get_view_corners():
def generate_user_camera() -> list:
""" Generate a basic camera represention of the user point of view
:return: list of 7 points
"""
area, region, rv3d = view3d_find() area, region, rv3d = view3d_find()
v1 = [0, 0, 0] v1 = v2 = v3 = v4 = v5 = v6 = v7 = [0, 0, 0]
v2 = [0, 0, 0]
v3 = [0, 0, 0]
v4 = [0, 0, 0]
v5 = [0, 0, 0]
v6 = [0, 0, 0]
v7 = [0, 0, 0]
if area and region and rv3d: if area and region and rv3d:
width = region.width width = region.width
height = region.height height = region.height
v1 = get_target(region, rv3d, (0, 0)) v1 = project_to_viewport(region, rv3d, (0, 0))
v3 = get_target(region, rv3d, (0, height)) v3 = project_to_viewport(region, rv3d, (0, height))
v2 = get_target(region, rv3d, (width, height)) v2 = project_to_viewport(region, rv3d, (width, height))
v4 = get_target(region, rv3d, (width, 0)) v4 = project_to_viewport(region, rv3d, (width, 0))
v5 = get_target(region, rv3d, (width/2, height/2)) v5 = project_to_viewport(region, rv3d, (width/2, height/2))
v6 = list(rv3d.view_location) v6 = list(rv3d.view_location)
v7 = get_target_far(region, rv3d, (width/2, height/2), -.8) v7 = project_to_viewport(
region, rv3d, (width/2, height/2), distance=-.8)
coords = [v1, v2, v3, v4, v5, v6, v7] coords = [v1, v2, v3, v4, v5, v6, v7]
return coords return coords
def get_client_2d(coords): def project_to_screen(coords: list) -> list:
""" Project 3D coordinate to 2D screen coordinates
:param coords: 3D coordinates (x,y,z)
:type coords: list
:return: list of 2D coordinates [x,y]
"""
area, region, rv3d = view3d_find() area, region, rv3d = view3d_find()
if area and region and rv3d: if area and region and rv3d:
return view3d_utils.location_3d_to_region_2d(region, rv3d, coords) return view3d_utils.location_3d_to_region_2d(region, rv3d, coords)
else: else:
return (0, 0) return (0, 0)
def get_bb_coords_from_obj(object, parent=None):
base = object.matrix_world if parent is None else parent.matrix_world def get_bb_coords_from_obj(object: bpy.types.Object, instance: bpy.types.Object = None) -> list:
""" Generate bounding box in world coordinate from object bound box
:param object: target object
:type object: bpy.types.Object
:param instance: optionnal instance
:type instance: bpy.types.Object
:return: list of 8 points [(x,y,z),...]
"""
base = object.matrix_world
if instance:
scale = mathutils.Matrix.Diagonal(object.matrix_world.to_scale())
base = instance.matrix_world @ scale.to_4x4()
bbox_corners = [base @ mathutils.Vector( bbox_corners = [base @ mathutils.Vector(
corner) for corner in object.bound_box] corner) for corner in object.bound_box]
return [(point.x, point.y, point.z) return [(point.x, point.y, point.z) for point in bbox_corners]
for point in bbox_corners]
def get_view_matrix(): def get_view_matrix() -> list:
""" Return the 3d viewport view matrix
:return: view matrix as a 4x4 list
"""
area, region, rv3d = view3d_find() area, region, rv3d = view3d_find()
if area and region and rv3d: if area and region and rv3d:
return [list(v) for v in rv3d.view_matrix] return [list(v) for v in rv3d.view_matrix]
def update_presence(self, context):
global renderer
if 'renderer' in globals() and hasattr(renderer, 'run'): class Widget(object):
if self.enable_presence: """ Base class to define an interface element
renderer.run() """
draw_type: str = 'POST_VIEW' # Draw event type
def poll(self) -> bool:
"""Test if the widget can be drawn or not
:return: bool
"""
return True
def draw(self):
"""How to draw the widget
"""
raise NotImplementedError()
class UserFrustumWidget(Widget):
# Camera widget indices
indices = ((1, 3), (2, 1), (3, 0),
(2, 0), (4, 5), (1, 6),
(2, 6), (3, 6), (0, 6))
def __init__(
self,
username):
self.username = username
self.settings = bpy.context.window_manager.session
@property
def data(self):
user = session.online_users.get(self.username)
if user:
return user.get('metadata')
else: else:
renderer.stop() return None
def poll(self):
if self.data is None:
return False
def update_overlay_settings(self, context): scene_current = self.data.get('scene_current')
global renderer view_corners = self.data.get('view_corners')
if renderer and not self.presence_show_selected: return (scene_current == bpy.context.scene.name or
renderer.flush_selection() self.settings.presence_show_far_user) and \
if renderer and not self.presence_show_user: view_corners and \
renderer.flush_users() self.settings.presence_show_user and \
self.settings.enable_presence
def draw(self):
class DrawFactory(object): location = self.data.get('view_corners')
def __init__(self):
self.d3d_items = {}
self.d2d_items = {}
self.draw3d_handle = None
self.draw2d_handle = None
self.draw_event = None
self.coords = None
self.active_object = None
def run(self):
self.register_handlers()
def stop(self):
self.flush_users()
self.flush_selection()
self.unregister_handlers()
refresh_3d_view()
def register_handlers(self):
self.draw3d_handle = bpy.types.SpaceView3D.draw_handler_add(
self.draw3d_callback, (), 'WINDOW', 'POST_VIEW')
self.draw2d_handle = bpy.types.SpaceView3D.draw_handler_add(
self.draw2d_callback, (), 'WINDOW', 'POST_PIXEL')
def unregister_handlers(self):
if self.draw2d_handle:
bpy.types.SpaceView3D.draw_handler_remove(
self.draw2d_handle, "WINDOW")
self.draw2d_handle = None
if self.draw3d_handle:
bpy.types.SpaceView3D.draw_handler_remove(
self.draw3d_handle, "WINDOW")
self.draw3d_handle = None
self.d3d_items.clear()
self.d2d_items.clear()
def flush_selection(self, user=None):
key_to_remove = []
select_key = f"{user}_select" if user else "select"
for k in self.d3d_items.keys():
if select_key in k:
key_to_remove.append(k)
for k in key_to_remove:
del self.d3d_items[k]
def flush_users(self):
key_to_remove = []
for k in self.d3d_items.keys():
if "select" not in k:
key_to_remove.append(k)
for k in key_to_remove:
del self.d3d_items[k]
self.d2d_items.clear()
def draw_client_selection(self, client_id, client_color, client_selection):
local_user = utils.get_preferences().username
if local_user != client_id:
self.flush_selection(client_id)
for select_ob in client_selection:
drawable_key = f"{client_id}_select_{select_ob}"
ob = utils.find_from_attr("uuid", select_ob, bpy.data.objects)
if not ob:
return
if ob.type == 'EMPTY':
# TODO: Child case
# Collection instance case
indices = (
(0, 1), (1, 2), (2, 3), (0, 3),
(4, 5), (5, 6), (6, 7), (4, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
if ob.instance_collection:
for obj in ob.instance_collection.objects:
if obj.type == 'MESH':
self.append_3d_item(
drawable_key,
client_color,
get_bb_coords_from_obj(obj, parent=ob),
indices)
if ob.type in ['MESH','META']:
indices = (
(0, 1), (1, 2), (2, 3), (0, 3),
(4, 5), (5, 6), (6, 7), (4, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
self.append_3d_item(
drawable_key,
client_color,
get_bb_coords_from_obj(ob),
indices)
else:
indices = (
(0, 1), (0, 2), (1, 3), (2, 3),
(4, 5), (4, 6), (5, 7), (6, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
self.append_3d_item(
drawable_key,
client_color,
get_default_bbox(ob, ob.scale.x),
indices)
def append_3d_item(self,key,color, coords, indices):
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR') shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
color = color positions = [tuple(coord) for coord in location]
if len(positions) != 7:
return
batch = batch_for_shader( batch = batch_for_shader(
shader, 'LINES', {"pos": coords}, indices=indices) shader,
'LINES',
{"pos": positions},
indices=self.indices)
self.d3d_items[key] = (shader, batch, color)
def draw_client_camera(self, client_id, client_location, client_color):
if client_location:
local_user = utils.get_preferences().username
if local_user != client_id:
try:
indices = (
(1, 3), (2, 1), (3, 0),
(2, 0), (4, 5), (1, 6),
(2, 6), (3, 6), (0, 6)
)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
position = [tuple(coord) for coord in client_location]
color = client_color
batch = batch_for_shader(
shader, 'LINES', {"pos": position}, indices=indices)
self.d3d_items[client_id] = (shader, batch, color)
self.d2d_items[client_id] = (position[1], client_id, color)
except Exception as e:
logging.debug(f"Draw client exception: {e} \n {traceback.format_exc()}\n pos:{position},ind:{indices}")
def draw3d_callback(self):
bgl.glLineWidth(2.) bgl.glLineWidth(2.)
bgl.glEnable(bgl.GL_DEPTH_TEST) bgl.glEnable(bgl.GL_DEPTH_TEST)
bgl.glEnable(bgl.GL_BLEND) bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH) bgl.glEnable(bgl.GL_LINE_SMOOTH)
shader.bind()
shader.uniform_float("color", self.data.get('color'))
batch.draw(shader)
class UserSelectionWidget(Widget):
def __init__(
self,
username):
self.username = username
self.settings = bpy.context.window_manager.session
@property
def data(self):
user = session.online_users.get(self.username)
if user:
return user.get('metadata')
else:
return None
def poll(self):
if self.data is None:
return False
user_selection = self.data.get('selected_objects')
scene_current = self.data.get('scene_current')
return (scene_current == bpy.context.scene.name or
self.settings.presence_show_far_user) and \
user_selection and \
self.settings.presence_show_selected and \
self.settings.enable_presence
def draw(self):
user_selection = self.data.get('selected_objects')
for select_ob in user_selection:
ob = find_from_attr("uuid", select_ob, bpy.data.objects)
if not ob:
return
vertex_pos = bbox_from_obj(ob, 1.0)
vertex_indices = ((0, 1), (0, 2), (1, 3), (2, 3),
(4, 5), (4, 6), (5, 7), (6, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
if ob.instance_collection:
for obj in ob.instance_collection.objects:
if obj.type == 'MESH' and hasattr(obj, 'bound_box'):
vertex_pos = get_bb_coords_from_obj(obj, instance=ob)
break
elif ob.type == 'EMPTY':
vertex_pos = bbox_from_obj(ob, ob.empty_display_size)
elif ob.type == 'LIGHT':
vertex_pos = bbox_from_obj(ob, ob.data.shadow_soft_size)
elif ob.type == 'LIGHT_PROBE':
vertex_pos = bbox_from_obj(ob, ob.data.influence_distance)
elif ob.type == 'CAMERA':
vertex_pos = bbox_from_obj(ob, ob.data.display_size)
elif hasattr(ob, 'bound_box'):
vertex_indices = (
(0, 1), (1, 2), (2, 3), (0, 3),
(4, 5), (5, 6), (6, 7), (4, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
vertex_pos = get_bb_coords_from_obj(ob)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader(
shader,
'LINES',
{"pos": vertex_pos},
indices=vertex_indices)
shader.bind()
shader.uniform_float("color", self.data.get('color'))
batch.draw(shader)
class UserNameWidget(Widget):
draw_type = 'POST_PIXEL'
def __init__(
self,
username):
self.username = username
self.settings = bpy.context.window_manager.session
@property
def data(self):
user = session.online_users.get(self.username)
if user:
return user.get('metadata')
else:
return None
def poll(self):
if self.data is None:
return False
scene_current = self.data.get('scene_current')
view_corners = self.data.get('view_corners')
return (scene_current == bpy.context.scene.name or
self.settings.presence_show_far_user) and \
view_corners and \
self.settings.presence_show_user and \
self.settings.enable_presence
def draw(self):
view_corners = self.data.get('view_corners')
color = self.data.get('color')
position = [tuple(coord) for coord in view_corners]
coords = project_to_screen(position[1])
if coords:
blf.position(0, coords[0], coords[1]+10, 0)
blf.size(0, 16, 72)
blf.color(0, color[0], color[1], color[2], color[3])
blf.draw(0, self.username)
class SessionStatusWidget(Widget):
draw_type = 'POST_PIXEL'
def __init__(self):
self.preferences = get_preferences()
@property
def settings(self):
return getattr(bpy.context.window_manager, 'session', None)
def poll(self):
return self.settings and self.settings.presence_show_session_status and \
self.settings.enable_presence
def draw(self):
text_scale = self.preferences.presence_hud_scale
ui_scale = bpy.context.preferences.view.ui_scale
color = [1, 1, 0, 1]
state = session.state.get('STATE')
state_str = f"{get_state_str(state)}"
if state == STATE_ACTIVE:
color = [0, 1, 0, 1]
elif state == STATE_INITIAL:
color = [1, 0, 0, 1]
hpos = (self.preferences.presence_hud_hpos*bpy.context.area.width)/100
vpos = (self.preferences.presence_hud_vpos*bpy.context.area.height)/100
blf.position(0, hpos, vpos, 0)
blf.size(0, int(text_scale*ui_scale), 72)
blf.color(0, color[0], color[1], color[2], color[3])
blf.draw(0, state_str)
class DrawFactory(object):
def __init__(self):
self.post_view_handle = None
self.post_pixel_handle = None
self.widgets = {}
def add_widget(self, name: str, widget: Widget):
self.widgets[name] = widget
def remove_widget(self, name: str):
if name in self.widgets:
del self.widgets[name]
else:
logging.error(f"Widget {name} not existing")
def clear_widgets(self):
self.widgets.clear()
def register_handlers(self):
self.post_view_handle = bpy.types.SpaceView3D.draw_handler_add(
self.post_view_callback,
(),
'WINDOW',
'POST_VIEW')
self.post_pixel_handle = bpy.types.SpaceView3D.draw_handler_add(
self.post_pixel_callback,
(),
'WINDOW',
'POST_PIXEL')
def unregister_handlers(self):
if self.post_pixel_handle:
bpy.types.SpaceView3D.draw_handler_remove(
self.post_pixel_handle,
"WINDOW")
self.post_pixel_handle = None
if self.post_view_handle:
bpy.types.SpaceView3D.draw_handler_remove(
self.post_view_handle,
"WINDOW")
self.post_view_handle = None
def post_view_callback(self):
try: try:
for shader, batch, color in self.d3d_items.values(): for widget in self.widgets.values():
shader.bind() if widget.draw_type == 'POST_VIEW' and widget.poll():
shader.uniform_float("color", color) widget.draw()
batch.draw(shader) except Exception as e:
except Exception: logging.error(
logging.error("3D Exception") f"Post view widget exception: {e} \n {traceback.print_exc()}")
def draw2d_callback(self): def post_pixel_callback(self):
for position, font, color in self.d2d_items.values(): try:
try: for widget in self.widgets.values():
coords = get_client_2d(position) if widget.draw_type == 'POST_PIXEL' and widget.poll():
widget.draw()
except Exception as e:
logging.error(
f"Post pixel widget Exception: {e} \n {traceback.print_exc()}")
if coords:
blf.position(0, coords[0], coords[1]+10, 0)
blf.size(0, 16, 72)
blf.color(0, color[0], color[1], color[2], color[3])
blf.draw(0, font)
except Exception: this = sys.modules[__name__]
logging.error("2D EXCEPTION") this.renderer = DrawFactory()
def register(): def register():
global renderer this.renderer.register_handlers()
renderer = DrawFactory()
this.renderer.add_widget("session_status", SessionStatusWidget())
def unregister(): def unregister():
global renderer this.renderer.unregister_handlers()
renderer.unregister_handlers()
del renderer this.renderer.clear_widgets()

View File

@ -18,7 +18,7 @@
import bpy import bpy
from .utils import get_preferences, get_expanded_icon, get_folder_size from .utils import get_preferences, get_expanded_icon, get_folder_size, get_state_str
from replication.constants import (ADDED, ERROR, FETCHED, from replication.constants import (ADDED, ERROR, FETCHED,
MODIFIED, RP_COMMON, UP, MODIFIED, RP_COMMON, UP,
STATE_ACTIVE, STATE_AUTH, STATE_ACTIVE, STATE_AUTH,
@ -34,9 +34,9 @@ ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
'TRIA_UP', # COMMITED 'TRIA_UP', # COMMITED
'KEYTYPE_KEYFRAME_VEC', # PUSHED 'KEYTYPE_KEYFRAME_VEC', # PUSHED
'TRIA_DOWN', # FETCHED 'TRIA_DOWN', # FETCHED
'FILE_REFRESH', # UP 'RECOVER_LAST', # RESET
'TRIA_UP', 'TRIA_UP', # CHANGED
'ERROR'] # CHANGED 'ERROR'] # ERROR
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=' '):
@ -60,32 +60,6 @@ def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=
return f"{prefix} |{bar}| {iteration}/{total}{suffix}" 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'
elif state == STATE_AUTH:
state_str = 'AUTHENTIFICATION'
elif state == STATE_CONFIG:
state_str = 'CONFIGURATION'
elif state == STATE_ACTIVE:
state_str = 'ONLINE'
elif state == STATE_SRV_SYNC:
state_str = 'PUSHING'
elif state == STATE_INITIAL:
state_str = 'INIT'
elif state == STATE_QUITTING:
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): class SESSION_PT_settings(bpy.types.Panel):
"""Settings panel""" """Settings panel"""
bl_idname = "MULTIUSER_SETTINGS_PT_panel" bl_idname = "MULTIUSER_SETTINGS_PT_panel"
@ -140,7 +114,7 @@ class SESSION_PT_settings(bpy.types.Panel):
if current_state in [STATE_ACTIVE] and runtime_settings.is_host: if current_state in [STATE_ACTIVE] and runtime_settings.is_host:
info_msg = f"LAN: {runtime_settings.internet_ip}" info_msg = f"LAN: {runtime_settings.internet_ip}"
if current_state == STATE_LOBBY: if current_state == STATE_LOBBY:
info_msg = "Waiting the session to start." info_msg = "Waiting for the session to start."
if info_msg: if info_msg:
info_box = row.box() info_box = row.box()
@ -474,8 +448,17 @@ class SESSION_PT_presence(bpy.types.Panel):
layout = self.layout layout = self.layout
settings = context.window_manager.session settings = context.window_manager.session
pref = get_preferences()
layout.active = settings.enable_presence layout.active = settings.enable_presence
col = layout.column() col = layout.column()
col.prop(settings, "presence_show_session_status")
row = col.column()
row.active = settings.presence_show_session_status
row.prop(pref, "presence_hud_scale", expand=True)
row = col.column(align=True)
row.active = settings.presence_show_session_status
row.prop(pref, "presence_hud_hpos", expand=True)
row.prop(pref, "presence_hud_vpos", expand=True)
col.prop(settings, "presence_show_selected") col.prop(settings, "presence_show_selected")
col.prop(settings, "presence_show_user") col.prop(settings, "presence_show_user")
row = layout.column() row = layout.column()
@ -517,10 +500,12 @@ def draw_property(context, parent, property_uuid, level=0):
detail_item_box.separator() detail_item_box.separator()
if item.state in [FETCHED, UP]: if item.state in [FETCHED, UP]:
detail_item_box.operator( apply = detail_item_box.operator(
"session.apply", "session.apply",
text="", text="",
icon=ICONS_PROP_STATES[item.state]).target = item.uuid icon=ICONS_PROP_STATES[item.state])
apply.target = item.uuid
apply.reset_dependencies = True
elif item.state in [MODIFIED, ADDED]: elif item.state in [MODIFIED, ADDED]:
detail_item_box.operator( detail_item_box.operator(
"session.commit", "session.commit",
@ -637,14 +622,15 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
display_all = overlay.show_overlays display_all = overlay.show_overlays
col = layout.column() col = layout.column()
col.active = display_all
row = col.row(align=True) row = col.row(align=True)
settings = context.window_manager.session settings = context.window_manager.session
layout.active = settings.enable_presence layout.active = settings.enable_presence
col = layout.column() col = layout.column()
col.prop(settings, "presence_show_session_status")
col.prop(settings, "presence_show_selected") col.prop(settings, "presence_show_selected")
col.prop(settings, "presence_show_user") col.prop(settings, "presence_show_user")
row = layout.column() row = layout.column()
row.active = settings.presence_show_user row.active = settings.presence_show_user
row.prop(settings, "presence_show_far_user") row.prop(settings, "presence_show_far_user")

View File

@ -29,7 +29,14 @@ import math
import bpy import bpy
import mathutils import mathutils
from . import environment, presence from . import environment
from replication.constants import (STATE_ACTIVE, STATE_AUTH,
STATE_CONFIG, STATE_SYNCING,
STATE_INITIAL, STATE_SRV_SYNC,
STATE_WAITING, STATE_QUITTING,
STATE_LOBBY,
STATE_LAUNCHING_SERVICES)
def find_from_attr(attr_name, attr_value, list): def find_from_attr(attr_name, attr_value, list):
@ -58,6 +65,32 @@ def get_datablock_users(datablock):
return users return users
def get_state_str(state):
state_str = 'UNKOWN'
if state == STATE_WAITING:
state_str = 'WARMING UP DATA'
elif state == STATE_SYNCING:
state_str = 'FETCHING'
elif state == STATE_AUTH:
state_str = 'AUTHENTICATION'
elif state == STATE_CONFIG:
state_str = 'CONFIGURATION'
elif state == STATE_ACTIVE:
state_str = 'ONLINE'
elif state == STATE_SRV_SYNC:
state_str = 'PUSHING'
elif state == STATE_INITIAL:
state_str = 'OFFLINE'
elif state == STATE_QUITTING:
state_str = 'QUITTING'
elif state == STATE_LAUNCHING_SERVICES:
state_str = 'LAUNCHING SERVICES'
elif state == STATE_LOBBY:
state_str = 'LOBBY'
return state_str
def clean_scene(): def clean_scene():
for type_name in dir(bpy.data): for type_name in dir(bpy.data):
try: try:

View File

@ -1,8 +1,8 @@
# Download base image debian jessie # Download base image debian jessie
FROM python:slim FROM python:slim
ARG replication_version=0.0.21a15 ARG replication_version=0.0.21
ARG version=0.1.0 ARG version=0.1.1
# Infos # Infos
LABEL maintainer="Swann Martinez" LABEL maintainer="Swann Martinez"

View File

@ -0,0 +1,10 @@
#! /bin/bash
# Start server in docker container, from image hosted on the multi-user gitlab's container registry
docker run -d \
-p 5555-5560:5555-5560 \
-e port=5555 \
-e log-level DEBUG \
-e password=admin \
-e timeout=1000 \
registry.gitlab.com/slumber/multi-user/multi-user-server:0.1.0

View File

@ -0,0 +1,5 @@
#! /bin/bash
# Start replication server locally, and include logging (requires replication_version=0.0.21a15)
clear
replication.serve -p 5555 -pwd admin -t 1000 -l DEBUG -lf server.log

View File

@ -7,13 +7,12 @@ import bpy
from multi_user.bl_types.bl_material import BlMaterial from multi_user.bl_types.bl_material import BlMaterial
def test_material(clear_blend): def test_material_nodes(clear_blend):
nodes_types = [node.bl_rna.identifier for node in bpy.types.ShaderNode.__subclasses__()] nodes_types = [node.bl_rna.identifier for node in bpy.types.ShaderNode.__subclasses__()]
datablock = bpy.data.materials.new("test") datablock = bpy.data.materials.new("test")
datablock.use_nodes = True datablock.use_nodes = True
bpy.data.materials.create_gpencil_data(datablock)
for ntype in nodes_types: for ntype in nodes_types:
datablock.node_tree.nodes.new(ntype) datablock.node_tree.nodes.new(ntype)
@ -26,3 +25,18 @@ def test_material(clear_blend):
result = implementation._dump(test) result = implementation._dump(test)
assert not DeepDiff(expected, result) assert not DeepDiff(expected, result)
def test_material_gpencil(clear_blend):
datablock = bpy.data.materials.new("test")
bpy.data.materials.create_gpencil_data(datablock)
implementation = BlMaterial()
expected = implementation._dump(datablock)
bpy.data.materials.remove(datablock)
test = implementation._construct(expected)
implementation._load(expected, test)
result = implementation._dump(test)
assert not DeepDiff(expected, result)

View File

@ -6,8 +6,11 @@ from deepdiff import DeepDiff
import bpy import bpy
import random import random
from multi_user.bl_types.bl_scene import BlScene from multi_user.bl_types.bl_scene import BlScene
from multi_user.utils import get_preferences
def test_scene(clear_blend): def test_scene(clear_blend):
get_preferences().sync_flags.sync_render_settings = True
datablock = bpy.data.scenes.new("toto") datablock = bpy.data.scenes.new("toto")
datablock.view_settings.use_curve_mapping = True datablock.view_settings.use_curve_mapping = True
# Test # Test