Compare commits
185 Commits
datablock_
...
revert-d42
Author | SHA1 | Date | |
---|---|---|---|
761fa26ed7 | |||
d42ed789e7 | |||
40cec39d27 | |||
498616147b | |||
30b2f5d32e | |||
f7e98abb59 | |||
4022f300b3 | |||
cef45dad3c | |||
30d734c2c1 | |||
4391510d7b | |||
04a4f7668a | |||
908c0fa4af | |||
c718e62b33 | |||
2f34bba1fd | |||
db4e495183 | |||
c00a7184ff | |||
9c83df45fc | |||
17949003f7 | |||
371d793a13 | |||
c710111887 | |||
664f7635cc | |||
babecf5ae7 | |||
0bad6895da | |||
66e55a7eec | |||
4e2377cd7f | |||
f90c12b27f | |||
3573db0969 | |||
92bde00a5a | |||
2c82560d24 | |||
6f364d2b88 | |||
760b52c02b | |||
4dd932fc56 | |||
ba1a03cbfa | |||
18b5fa795c | |||
1a82ec72e4 | |||
804747c73b | |||
7ee705332f | |||
bed33ca6ba | |||
716c78e380 | |||
5e4ce4556f | |||
aa9ea08151 | |||
f56890128e | |||
8865556229 | |||
5bc9b10c12 | |||
7db3c18213 | |||
ff35e34032 | |||
9f8222afa7 | |||
1828bfac22 | |||
3a1087ecb8 | |||
b398541787 | |||
f0b33d8471 | |||
5a282a3e22 | |||
4283fc0fff | |||
753f4d3f27 | |||
9dd02b2756 | |||
c74d12c843 | |||
e1d9982276 | |||
8861986213 | |||
1cb9fb410c | |||
c4a8cc4606 | |||
187f11071c | |||
530fae8cb4 | |||
6771c371a1 | |||
c844c6e54f | |||
a4d0b1a68b | |||
2fdc11692d | |||
dbfca4568f | |||
069a528276 | |||
030f2661fd | |||
e589e3eec4 | |||
04140ced1b | |||
0d9ce43e74 | |||
d3969b4fd4 | |||
e21f64ac98 | |||
b25b380d21 | |||
1146d9d304 | |||
51b60521e6 | |||
035f8a1dcd | |||
cefaef5c4b | |||
4714e60ff7 | |||
3eca25ae19 | |||
96346f8a25 | |||
a258c2c182 | |||
6862df5331 | |||
f271a9d0e3 | |||
bdff6eb5c9 | |||
b661407952 | |||
d5eb7fda02 | |||
35e8ac9c33 | |||
4453d256b8 | |||
299e330ec6 | |||
34b9f7ae27 | |||
9d100d84ad | |||
2f677c399e | |||
e967b35c38 | |||
7bd0a196b4 | |||
7892b5e9b6 | |||
f779678c0e | |||
629fc2d223 | |||
724c2345df | |||
673c4e69a4 | |||
fbfff6c7ec | |||
f592294335 | |||
8e7be5afde | |||
fc76b2a8e6 | |||
1a8bcddb74 | |||
60fba5b9df | |||
be0eb1fa42 | |||
93d9bea3ae | |||
022b7f7822 | |||
ae34846509 | |||
d328077cb0 | |||
0c4740eef8 | |||
d7b2c7e2f6 | |||
efbb9e7096 | |||
e0b56d8990 | |||
7a94c21187 | |||
0687090f05 | |||
920744334c | |||
dfa7f98126 | |||
ea530f0f96 | |||
c3546ff74f | |||
83aa9b57ec | |||
28a265be68 | |||
7dfabb16c7 | |||
ea5d9371ca | |||
3df73a0716 | |||
ae3c994ff1 | |||
bd73b385b6 | |||
f054b1c5f2 | |||
d083100a2a | |||
b813b8df9e | |||
d0e966ff1a | |||
56cbf14fe1 | |||
8bf55ebd46 | |||
edbc5ee343 | |||
4a92511582 | |||
b42df2cf4a | |||
7549466824 | |||
423e71476d | |||
3bc4b20035 | |||
9966a24b5e | |||
577c01a594 | |||
3d72796c10 | |||
edcbd7b02a | |||
b368c985b8 | |||
cab1a71eaa | |||
33cb188509 | |||
0a3dd9b5b8 | |||
7fbdbdcc21 | |||
8f9d5aabf9 | |||
824d4d6a83 | |||
5f4bccbcd9 | |||
8e8e54fe7d | |||
04b13cc0b7 | |||
ba98875560 | |||
a9fb84a5c6 | |||
2f139178d3 | |||
e466f81600 | |||
cb836e30f5 | |||
152e356dad | |||
7b13e8978b | |||
e0839fe1fb | |||
aec3e8b8bf | |||
a89564de6b | |||
e301a10456 | |||
cfc6ce91bc | |||
4f731c6640 | |||
9b1b8f11fd | |||
e742c824fc | |||
6757bbbd30 | |||
f6a39e4290 | |||
410d8d2f1a | |||
bd64c17f05 | |||
dc063b5954 | |||
0ae34d5702 | |||
167b39f15e | |||
9adc0d7d6e | |||
fb622fa098 | |||
c533d4b86a | |||
6c47e095be | |||
f992d06b03 | |||
af3afc1124 | |||
b77ab2dd05 | |||
150054d19c |
@ -1,7 +1,10 @@
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
|
||||
|
||||
include:
|
||||
- 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
|
||||
|
@ -3,13 +3,11 @@ build:
|
||||
image: debian:stable-slim
|
||||
script:
|
||||
- rm -rf tests .git .gitignore script
|
||||
|
||||
artifacts:
|
||||
name: multi_user
|
||||
paths:
|
||||
- multi_user
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
- develop
|
||||
|
||||
- master
|
||||
- develop
|
23
.gitlab/ci/deploy.gitlab-ci.yml
Normal file
@ -0,0 +1,23 @@
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: slumber/docker-python
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
|
||||
services:
|
||||
- docker:19.03.12-dind
|
||||
|
||||
script:
|
||||
- RP_VERSION="$(python scripts/get_replication_version.py)"
|
||||
- VERSION="$(python scripts/get_addon_version.py)"
|
||||
- echo "Building docker image with replication ${RP_VERSION}"
|
||||
- docker build --build-arg replication_version=${RP_VERSION} --build-arg version={VERSION} -t registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION} ./scripts/docker_server
|
||||
- echo "Pushing to gitlab registry ${VERSION}"
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker push registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION}
|
||||
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
- develop
|
@ -3,8 +3,3 @@ test:
|
||||
image: slumber/blender-addon-testing:latest
|
||||
script:
|
||||
- python3 scripts/test_addon.py
|
||||
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
- develop
|
||||
|
51
CHANGELOG.md
@ -65,24 +65,63 @@ All notable changes to this project will be documented in this file.
|
||||
- Unused strict right management strategy
|
||||
- Legacy config management system
|
||||
|
||||
## [0.0.4] - preview
|
||||
## [0.1.0] - 2020-10-05
|
||||
|
||||
### Added
|
||||
|
||||
- Dependency graph driven updates [experimental]
|
||||
- Optional Edit Mode update
|
||||
- Edit Mode updates
|
||||
- Late join mechanism
|
||||
- Sync Axis lock replication
|
||||
- Sync collection offset
|
||||
- Sync camera orthographic scale
|
||||
- Logging basic configuration (file output and level)
|
||||
- Sync custom fonts
|
||||
- Sync sound files
|
||||
- Logging configuration (file output and level)
|
||||
- Object visibility type replication
|
||||
- Optionnal sync for active camera
|
||||
- Curve->Mesh conversion
|
||||
- Mesh->gpencil conversion
|
||||
|
||||
### Changed
|
||||
|
||||
- Auto updater now handle installation from branches
|
||||
- use uuid for collection loading
|
||||
- Use uuid for collection loading
|
||||
- Moved session instance to replication package
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent unsuported datatypes to crash the session
|
||||
- Modifier vertex group assignation
|
||||
- Prevent unsupported data types to crash the session
|
||||
- Modifier vertex group assignation
|
||||
- World sync
|
||||
- Snapshot UUID error
|
||||
- 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)
|
||||
|
49
README.md
@ -25,27 +25,32 @@ See the [documentation](https://multi-user.readthedocs.io/en/latest/) for detail
|
||||
|
||||
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
|
||||
|
||||
| Name | Status | Comment |
|
||||
| ----------- | :----: | :-----------------------------------------------------------: |
|
||||
| action | ❗ | Not stable |
|
||||
| armature | ❗ | Not stable |
|
||||
| camera | ✔️ | |
|
||||
| collection | ✔️ | |
|
||||
| curve | ✔️ | Nurbs surface don't load correctly |
|
||||
| gpencil | ✔️ | |
|
||||
| image | ❗ | Not stable yet |
|
||||
| mesh | ✔️ | |
|
||||
| material | ✔️ | |
|
||||
| metaball | ✔️ | |
|
||||
| object | ✔️ | |
|
||||
| scene | ✔️ | |
|
||||
| world | ✔️ | |
|
||||
| lightprobes | ✔️ | |
|
||||
| particles | ❌ | [On-going](https://gitlab.com/slumber/multi-user/-/issues/24) |
|
||||
| speakers | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/65) |
|
||||
| vse | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
|
||||
| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
|
||||
| libraries | ❗ | Partial |
|
||||
| Name | Status | Comment |
|
||||
| ----------- | :----: | :--------------------------------------------------------------------------: |
|
||||
| action | ✔️ | |
|
||||
| armature | ❗ | Not stable |
|
||||
| camera | ✔️ | |
|
||||
| collection | ✔️ | |
|
||||
| curve | ❗ | Nurbs not supported |
|
||||
| gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) |
|
||||
| image | ✔️ | |
|
||||
| mesh | ✔️ | |
|
||||
| material | ✔️ | |
|
||||
| metaball | ✔️ | |
|
||||
| object | ✔️ | |
|
||||
| texts | ✔️ | |
|
||||
| scene | ✔️ | |
|
||||
| world | ✔️ | |
|
||||
| lightprobes | ✔️ | |
|
||||
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
|
||||
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
|
||||
| nla | ❌ | |
|
||||
| volumes | ❌ | |
|
||||
| particles | ❌ | [On-going](https://gitlab.com/slumber/multi-user/-/issues/24) |
|
||||
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
|
||||
| vse | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
|
||||
| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
|
||||
| libraries | ❗ | Partial |
|
||||
|
||||
|
||||
### Performance issues
|
||||
@ -57,7 +62,7 @@ I'm working on it.
|
||||
|
||||
| Dependencies | Version | Needed |
|
||||
| ------------ | :-----: | -----: |
|
||||
| Replication | latest | yes |
|
||||
| Replication | latest | yes |
|
||||
|
||||
|
||||
|
||||
|
@ -22,7 +22,7 @@ copyright = '2020, Swann Martinez'
|
||||
author = 'Swann Martinez'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.0.2'
|
||||
release = '0.1.0'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.4 KiB |
BIN
docs/getting_started/img/quickstart_advanced_cache.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 559 B |
BIN
docs/getting_started/img/quickstart_replication.png
Normal file
After Width: | Height: | Size: 15 KiB |
@ -161,6 +161,19 @@ The collaboration quality directly depend on the communication quality. This sec
|
||||
various tools made in an effort to ease the communication between the different session users.
|
||||
Feel free to suggest any idea for communication tools `here <https://gitlab.com/slumber/multi-user/-/issues/75>`_ .
|
||||
|
||||
---------------------------
|
||||
Change replication behavior
|
||||
---------------------------
|
||||
|
||||
During a session, the multi-user will replicate your modifications to other instances.
|
||||
In order to avoid annoying other users when you are experimenting, some of those modifications can be ignored via
|
||||
various flags present at the top of the panel (see red area in the image bellow). Those flags are explained in the :ref:`replication` section.
|
||||
|
||||
.. figure:: img/quickstart_replication.png
|
||||
:align: center
|
||||
|
||||
Session replication flags
|
||||
|
||||
--------------------
|
||||
Monitor online users
|
||||
--------------------
|
||||
@ -242,6 +255,8 @@ various drawn parts via the following flags:
|
||||
- **Show users**: display users current viewpoint
|
||||
- **Show different scenes**: display users working on other scenes
|
||||
|
||||
|
||||
|
||||
-----------
|
||||
Manage data
|
||||
-----------
|
||||
@ -330,6 +345,8 @@ of the multi-user are using the same IPC port it will create conflict !
|
||||
**Timeout (in milliseconds)** is the maximum ping authorized before auto-disconnecting.
|
||||
You should only increase it if you have a bad connection.
|
||||
|
||||
.. _replication:
|
||||
|
||||
-----------
|
||||
Replication
|
||||
-----------
|
||||
@ -341,6 +358,8 @@ Replication
|
||||
|
||||
**Synchronize render settings** (only host) enable replication of EEVEE and CYCLES render settings to match render between clients.
|
||||
|
||||
**Synchronize active camera** sync the scene active camera.
|
||||
|
||||
**Edit Mode Updates** enable objects update while you are in Edit_Mode.
|
||||
|
||||
.. warning:: Edit Mode Updates kill performances with complex objects (heavy meshes, gpencil, etc...).
|
||||
@ -355,6 +374,26 @@ Replication
|
||||
- **Refresh**: pushed data update rate (in second)
|
||||
- **Apply**: pulled data update rate (in second)
|
||||
|
||||
-----
|
||||
Cache
|
||||
-----
|
||||
|
||||
The multi-user allows to replicate external blend dependencies such as images, movies sounds.
|
||||
On each client, those files are stored into the cache folder.
|
||||
|
||||
.. figure:: img/quickstart_advanced_cache.png
|
||||
:align: center
|
||||
|
||||
Advanced cache settings
|
||||
|
||||
**cache_directory** allows to choose where cached files (images, sound, movies) will be saved.
|
||||
|
||||
**Clear memory filecache** will save memory space at runtime by removing the file content from memory as soon as it have been written to the disk.
|
||||
|
||||
**Clear cache** will remove all file from the cache folder.
|
||||
|
||||
.. warning:: Clear cash could break your scene image/movie/sound if they are used into the blend !
|
||||
|
||||
---
|
||||
Log
|
||||
---
|
||||
|
@ -144,7 +144,7 @@ Let's check the connection status. Right click on the tray icon and click on **S
|
||||
Network status.
|
||||
|
||||
The network status must be **OK** for each user(like in the picture above) otherwise it means that you are not connected to the network.
|
||||
If you see something like **ACCESS_DENIED**, it means that you were not authorized to join the network. Please check the :ref:`network-authorization` section.
|
||||
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 !
|
||||
|
||||
@ -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.
|
||||
|
||||
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:`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:
|
||||
|
||||
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).
|
||||
2. Install the replication library:
|
||||
2. Install the latest version of the replication library:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m pip install replication
|
||||
python -m pip install replication==0.0.21a15
|
||||
|
||||
4. Launch the server with:
|
||||
|
||||
@ -199,17 +201,20 @@ You can run the dedicated server on any platform by following those steps:
|
||||
replication.serve
|
||||
|
||||
.. 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
|
||||
|
||||
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::
|
||||
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:
|
||||
@ -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
|
||||
---------------------------------------------
|
||||
|
||||
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
|
||||
|
||||
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.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.
|
||||
You can check the :ref:`how-to-join` section.
|
||||
As soon as the dedicated server is running, you can connect to it from blender by following :ref:`how-to-join`.
|
||||
|
||||
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::
|
||||
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:
|
||||
|
||||
|
@ -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>`_
|
||||
|
||||
--------------------------
|
||||
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.
|
||||
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:
|
||||
|
||||
@ -35,8 +35,75 @@ Here are some useful information you should provide in a bug report:
|
||||
Contributing code
|
||||
=================
|
||||
|
||||
1. Fork it (https://gitlab.com/yourname/yourproject/fork)
|
||||
2. Create your feature branch (git checkout -b feature/fooBar)
|
||||
3. Commit your changes (git commit -am 'Add some fooBar')
|
||||
4. Push to the branch (git push origin feature/fooBar)
|
||||
5. Create a new Pull Request
|
||||
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.
|
||||
The following example suggests how to contribute a feature.
|
||||
|
||||
1. Fork the project into a new repository:
|
||||
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 doesn’t 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.
|
@ -19,7 +19,7 @@
|
||||
bl_info = {
|
||||
"name": "Multi-User",
|
||||
"author": "Swann Martinez",
|
||||
"version": (0, 0, 3),
|
||||
"version": (0, 1, 1),
|
||||
"description": "Enable real-time collaborative workflow inside blender",
|
||||
"blender": (2, 82, 0),
|
||||
"location": "3D View > Sidebar > Multi-User tab",
|
||||
@ -40,14 +40,16 @@ import sys
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from . import environment, utils
|
||||
from . import environment
|
||||
|
||||
|
||||
DEPENDENCIES = {
|
||||
("replication", '0.0.21a8'),
|
||||
("replication", '0.1.3'),
|
||||
}
|
||||
|
||||
|
||||
module_error_msg = "Insufficient rights to install the multi-user \
|
||||
dependencies, aunch blender with administrator rights."
|
||||
def register():
|
||||
# Setup logging policy
|
||||
logging.basicConfig(
|
||||
@ -57,22 +59,22 @@ def register():
|
||||
|
||||
try:
|
||||
environment.setup(DEPENDENCIES, bpy.app.binary_path_python)
|
||||
except ModuleNotFoundError:
|
||||
logging.fatal("Fail to install multi-user dependencies, try to execute blender with admin rights.")
|
||||
return
|
||||
|
||||
from . import presence
|
||||
from . import operators
|
||||
from . import ui
|
||||
from . import preferences
|
||||
from . import addon_updater_ops
|
||||
|
||||
preferences.register()
|
||||
addon_updater_ops.register(bl_info)
|
||||
presence.register()
|
||||
operators.register()
|
||||
ui.register()
|
||||
from . import presence
|
||||
from . import operators
|
||||
from . import ui
|
||||
from . import preferences
|
||||
from . import addon_updater_ops
|
||||
|
||||
preferences.register()
|
||||
addon_updater_ops.register(bl_info)
|
||||
presence.register()
|
||||
operators.register()
|
||||
ui.register()
|
||||
except ModuleNotFoundError as e:
|
||||
raise Exception(module_error_msg)
|
||||
logging.error(module_error_msg)
|
||||
|
||||
bpy.types.WindowManager.session = bpy.props.PointerProperty(
|
||||
type=preferences.SessionProps)
|
||||
bpy.types.ID.uuid = bpy.props.StringProperty(
|
||||
|
@ -36,7 +36,8 @@ __all__ = [
|
||||
'bl_lightprobe',
|
||||
'bl_speaker',
|
||||
'bl_font',
|
||||
'bl_sound'
|
||||
'bl_sound',
|
||||
'bl_file'
|
||||
] # Order here defines execution order
|
||||
|
||||
from . import *
|
||||
|
@ -46,6 +46,98 @@ SPLINE_POINT = [
|
||||
"radius",
|
||||
]
|
||||
|
||||
CURVE_METADATA = [
|
||||
'align_x',
|
||||
'align_y',
|
||||
'bevel_depth',
|
||||
'bevel_factor_end',
|
||||
'bevel_factor_mapping_end',
|
||||
'bevel_factor_mapping_start',
|
||||
'bevel_factor_start',
|
||||
'bevel_object',
|
||||
'bevel_resolution',
|
||||
'body',
|
||||
'body_format',
|
||||
'dimensions',
|
||||
'eval_time',
|
||||
'extrude',
|
||||
'family',
|
||||
'fill_mode',
|
||||
'follow_curve',
|
||||
'font',
|
||||
'font_bold',
|
||||
'font_bold_italic',
|
||||
'font_italic',
|
||||
'make_local',
|
||||
'materials',
|
||||
'name',
|
||||
'offset',
|
||||
'offset_x',
|
||||
'offset_y',
|
||||
'overflow',
|
||||
'original',
|
||||
'override_create',
|
||||
'override_library',
|
||||
'path_duration',
|
||||
'preview',
|
||||
'render_resolution_u',
|
||||
'render_resolution_v',
|
||||
'resolution_u',
|
||||
'resolution_v',
|
||||
'shape_keys',
|
||||
'shear',
|
||||
'size',
|
||||
'small_caps_scale',
|
||||
'space_character',
|
||||
'space_line',
|
||||
'space_word',
|
||||
'type',
|
||||
'taper_object',
|
||||
'texspace_location',
|
||||
'texspace_size',
|
||||
'transform',
|
||||
'twist_mode',
|
||||
'twist_smooth',
|
||||
'underline_height',
|
||||
'underline_position',
|
||||
'use_auto_texspace',
|
||||
'use_deform_bounds',
|
||||
'use_fake_user',
|
||||
'use_fill_caps',
|
||||
'use_fill_deform',
|
||||
'use_map_taper',
|
||||
'use_path',
|
||||
'use_path_follow',
|
||||
'use_radius',
|
||||
'use_stretch',
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
SPLINE_METADATA = [
|
||||
'hide',
|
||||
'material_index',
|
||||
# 'order_u',
|
||||
# 'order_v',
|
||||
# 'point_count_u',
|
||||
# 'point_count_v',
|
||||
'points',
|
||||
'radius_interpolation',
|
||||
'resolution_u',
|
||||
'resolution_v',
|
||||
'tilt_interpolation',
|
||||
'type',
|
||||
'use_bezier_u',
|
||||
'use_bezier_v',
|
||||
'use_cyclic_u',
|
||||
'use_cyclic_v',
|
||||
'use_endpoint_u',
|
||||
'use_endpoint_v',
|
||||
'use_smooth',
|
||||
]
|
||||
|
||||
|
||||
class BlCurve(BlDatablock):
|
||||
bl_id = "curves"
|
||||
bl_class = bpy.types.Curve
|
||||
@ -62,12 +154,8 @@ class BlCurve(BlDatablock):
|
||||
loader = Loader()
|
||||
loader.load(target, data)
|
||||
|
||||
# if isinstance(curve, T.TextCurve):
|
||||
# curve.font = data['font']
|
||||
# curve.font_bold = data['font']
|
||||
# curve.font_bold_italic = data['font']
|
||||
# curve.font_italic = data['font']
|
||||
target.splines.clear()
|
||||
|
||||
# load splines
|
||||
for spline in data['splines'].values():
|
||||
new_spline = target.splines.new(spline['type'])
|
||||
@ -78,8 +166,12 @@ class BlCurve(BlDatablock):
|
||||
bezier_points = new_spline.bezier_points
|
||||
bezier_points.add(spline['bezier_points_count'])
|
||||
np_load_collection(spline['bezier_points'], bezier_points, SPLINE_BEZIER_POINT)
|
||||
|
||||
# Not really working for now...
|
||||
|
||||
if new_spline.type == 'POLY':
|
||||
points = new_spline.points
|
||||
points.add(spline['points_count'])
|
||||
np_load_collection(spline['points'], points, SPLINE_POINT)
|
||||
# Not working for now...
|
||||
# See https://blender.stackexchange.com/questions/7020/create-nurbs-surface-with-python
|
||||
if new_spline.type == 'NURBS':
|
||||
logging.error("NURBS not supported.")
|
||||
@ -95,6 +187,8 @@ class BlCurve(BlDatablock):
|
||||
dumper = Dumper()
|
||||
# Conflicting attributes
|
||||
# TODO: remove them with the NURBS support
|
||||
dumper.include_filter = CURVE_METADATA
|
||||
|
||||
dumper.exclude_filter = [
|
||||
'users',
|
||||
'order_u',
|
||||
@ -112,8 +206,13 @@ class BlCurve(BlDatablock):
|
||||
|
||||
for index, spline in enumerate(instance.splines):
|
||||
dumper.depth = 2
|
||||
dumper.include_filter = SPLINE_METADATA
|
||||
spline_data = dumper.dump(spline)
|
||||
# spline_data['points'] = np_dump_collection(spline.points, SPLINE_POINT)
|
||||
|
||||
if spline.type == 'POLY':
|
||||
spline_data['points_count'] = len(spline.points)-1
|
||||
spline_data['points'] = np_dump_collection(spline.points, SPLINE_POINT)
|
||||
|
||||
spline_data['bezier_points_count'] = len(spline.bezier_points)-1
|
||||
spline_data['bezier_points'] = np_dump_collection(spline.bezier_points, SPLINE_BEZIER_POINT)
|
||||
data['splines'][index] = spline_data
|
||||
|
@ -16,14 +16,16 @@
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
import logging
|
||||
from replication.constants import DIFF_BINARY, UP
|
||||
from replication.data import ReplicatedDatablock
|
||||
|
||||
from .. import utils
|
||||
from .dump_anything import Loader, Dumper
|
||||
from replication.data import ReplicatedDatablock
|
||||
from replication.constants import (UP, DIFF_BINARY)
|
||||
from .dump_anything import Dumper, Loader
|
||||
|
||||
|
||||
def has_action(target):
|
||||
@ -87,6 +89,19 @@ def load_driver(target_datablock, src_driver):
|
||||
loader.load(new_point, src_driver['keyframe_points'][src_point])
|
||||
|
||||
|
||||
def get_datablock_from_uuid(uuid, default, ignore=[]):
|
||||
if not uuid:
|
||||
return default
|
||||
|
||||
for category in dir(bpy.data):
|
||||
root = getattr(bpy.data, category)
|
||||
if isinstance(root, Iterable) and category not in ignore:
|
||||
for item in root:
|
||||
if getattr(item, 'uuid', None) == uuid:
|
||||
return item
|
||||
return default
|
||||
|
||||
|
||||
class BlDatablock(ReplicatedDatablock):
|
||||
"""BlDatablock
|
||||
|
||||
@ -113,7 +128,7 @@ class BlDatablock(ReplicatedDatablock):
|
||||
if instance and hasattr(instance, 'uuid'):
|
||||
instance.uuid = self.uuid
|
||||
|
||||
# self.diff_method = DIFF_BINARY
|
||||
self.diff_method = DIFF_BINARY
|
||||
|
||||
def resolve(self):
|
||||
datablock_ref = None
|
||||
|
143
multi_user/bl_types/bl_file.py
Normal file
@ -0,0 +1,143 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
from replication.constants import DIFF_BINARY, UP
|
||||
from replication.data import ReplicatedDatablock
|
||||
|
||||
from .. import utils
|
||||
from .dump_anything import Dumper, Loader
|
||||
|
||||
|
||||
def get_filepath(filename):
|
||||
"""
|
||||
Construct the local filepath
|
||||
"""
|
||||
return str(Path(
|
||||
utils.get_preferences().cache_directory,
|
||||
filename
|
||||
))
|
||||
|
||||
|
||||
def ensure_unpacked(datablock):
|
||||
if datablock.packed_file:
|
||||
logging.info(f"Unpacking {datablock.name}")
|
||||
|
||||
filename = Path(bpy.path.abspath(datablock.filepath)).name
|
||||
datablock.filepath = get_filepath(filename)
|
||||
|
||||
datablock.unpack(method="WRITE_ORIGINAL")
|
||||
|
||||
|
||||
class BlFile(ReplicatedDatablock):
|
||||
bl_id = 'file'
|
||||
bl_name = "file"
|
||||
bl_class = Path
|
||||
bl_delay_refresh = 0
|
||||
bl_delay_apply = 1
|
||||
bl_automatic_push = True
|
||||
bl_check_common = False
|
||||
bl_icon = 'FILE'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.instance = kwargs.get('instance', None)
|
||||
|
||||
if self.instance and not self.instance.exists():
|
||||
raise FileNotFoundError(str(self.instance))
|
||||
|
||||
self.preferences = utils.get_preferences()
|
||||
self.diff_method = DIFF_BINARY
|
||||
|
||||
def resolve(self):
|
||||
if self.data:
|
||||
self.instance = Path(get_filepath(self.data['name']))
|
||||
|
||||
if not self.instance.exists():
|
||||
logging.debug("File don't exist, loading it.")
|
||||
self._load(self.data, self.instance)
|
||||
|
||||
def push(self, socket, identity=None):
|
||||
super().push(socket, identity=None)
|
||||
|
||||
if self.preferences.clear_memory_filecache:
|
||||
del self.data['file']
|
||||
|
||||
def _dump(self, instance=None):
|
||||
"""
|
||||
Read the file and return a dict as:
|
||||
{
|
||||
name : filename
|
||||
extension :
|
||||
file: file content
|
||||
}
|
||||
"""
|
||||
logging.info(f"Extracting file metadata")
|
||||
|
||||
data = {
|
||||
'name': self.instance.name,
|
||||
}
|
||||
|
||||
logging.info(
|
||||
f"Reading {self.instance.name} content: {self.instance.stat().st_size} bytes")
|
||||
|
||||
try:
|
||||
file = open(self.instance, "rb")
|
||||
data['file'] = file.read()
|
||||
|
||||
file.close()
|
||||
except IOError:
|
||||
logging.warning(f"{self.instance} doesn't exist, skipping")
|
||||
else:
|
||||
file.close()
|
||||
|
||||
return data
|
||||
|
||||
def _load(self, data, target):
|
||||
"""
|
||||
Writing the file
|
||||
"""
|
||||
# TODO: check for empty data
|
||||
|
||||
if target.exists() and not self.diff():
|
||||
logging.info(f"{data['name']} already on the disk, skipping.")
|
||||
return
|
||||
try:
|
||||
file = open(target, "wb")
|
||||
file.write(data['file'])
|
||||
|
||||
if self.preferences.clear_memory_filecache:
|
||||
del self.data['file']
|
||||
except IOError:
|
||||
logging.warning(f"{target} doesn't exist, skipping")
|
||||
else:
|
||||
file.close()
|
||||
|
||||
def diff(self):
|
||||
if self.preferences.clear_memory_filecache:
|
||||
return False
|
||||
else:
|
||||
memory_size = sys.getsizeof(self.data['file'])-33
|
||||
disk_size = self.instance.stat().st_size
|
||||
return memory_size == disk_size
|
@ -16,14 +16,16 @@
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
import os
|
||||
import logging
|
||||
import pathlib
|
||||
from .. import utils
|
||||
from .dump_anything import Loader, Dumper
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from .bl_datablock import BlDatablock
|
||||
from .bl_file import get_filepath, ensure_unpacked
|
||||
from .dump_anything import Dumper, Loader
|
||||
|
||||
|
||||
class BlFont(BlDatablock):
|
||||
bl_id = "fonts"
|
||||
@ -35,35 +37,38 @@ class BlFont(BlDatablock):
|
||||
bl_icon = 'FILE_FONT'
|
||||
|
||||
def _construct(self, data):
|
||||
if data['filepath'] == '<builtin>':
|
||||
return bpy.data.fonts.load(data['filepath'])
|
||||
elif 'font_file' in data.keys():
|
||||
prefs = utils.get_preferences()
|
||||
ext = pathlib.Path(data['filepath']).suffix
|
||||
font_name = f"{self.uuid}{ext}"
|
||||
font_path = os.path.join(prefs.cache_directory, font_name)
|
||||
|
||||
os.makedirs(prefs.cache_directory, exist_ok=True)
|
||||
file = open(font_path, 'wb')
|
||||
file.write(data["font_file"])
|
||||
file.close()
|
||||
filename = data.get('filename')
|
||||
|
||||
logging.info(f'loading {font_path}')
|
||||
return bpy.data.fonts.load(font_path)
|
||||
if filename == '<builtin>':
|
||||
return bpy.data.fonts.load(filename)
|
||||
else:
|
||||
return bpy.data.fonts.load(get_filepath(filename))
|
||||
|
||||
def _load(self, data, target):
|
||||
pass
|
||||
|
||||
def _dump(self, instance=None):
|
||||
data = {
|
||||
'filepath':instance.filepath,
|
||||
'name':instance.name
|
||||
if instance.filepath == '<builtin>':
|
||||
filename = '<builtin>'
|
||||
else:
|
||||
filename = Path(instance.filepath).name
|
||||
|
||||
if not filename:
|
||||
raise FileExistsError(instance.filepath)
|
||||
|
||||
return {
|
||||
'filename': filename,
|
||||
'name': instance.name
|
||||
}
|
||||
if instance.filepath != '<builtin>' and not instance.is_embedded_data:
|
||||
file = open(instance.filepath, "rb")
|
||||
data['font_file'] = file.read()
|
||||
file.close()
|
||||
return data
|
||||
|
||||
def diff(self):
|
||||
return False
|
||||
|
||||
def _resolve_deps_implementation(self):
|
||||
deps = []
|
||||
if self.instance.filepath and self.instance.filepath != '<builtin>':
|
||||
ensure_unpacked(self.instance)
|
||||
|
||||
deps.append(Path(bpy.path.abspath(self.instance.filepath)))
|
||||
|
||||
return deps
|
||||
|
@ -16,13 +16,17 @@
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
import os
|
||||
import logging
|
||||
|
||||
from .. import utils
|
||||
from .dump_anything import Loader, Dumper
|
||||
from .bl_datablock import BlDatablock
|
||||
from .dump_anything import Dumper, Loader
|
||||
from .bl_file import get_filepath, ensure_unpacked
|
||||
|
||||
format_to_ext = {
|
||||
'BMP': 'bmp',
|
||||
@ -53,29 +57,6 @@ class BlImage(BlDatablock):
|
||||
bl_check_common = False
|
||||
bl_icon = 'IMAGE_DATA'
|
||||
|
||||
def dump_image(self, image):
|
||||
pixels = None
|
||||
if image.source == "GENERATED" or image.packed_file is not None:
|
||||
prefs = utils.get_preferences()
|
||||
img_name = f"{self.uuid}.{format_to_ext[image.file_format]}"
|
||||
|
||||
# Cache the image on the disk
|
||||
image.filepath_raw = os.path.join(prefs.cache_directory, img_name)
|
||||
os.makedirs(prefs.cache_directory, exist_ok=True)
|
||||
image.save()
|
||||
|
||||
if image.source == "FILE":
|
||||
image_path = bpy.path.abspath(image.filepath_raw)
|
||||
image_directory = os.path.dirname(image_path)
|
||||
os.makedirs(image_directory, exist_ok=True)
|
||||
image.save()
|
||||
file = open(image_path, "rb")
|
||||
pixels = file.read()
|
||||
file.close()
|
||||
else:
|
||||
raise ValueError()
|
||||
return pixels
|
||||
|
||||
def _construct(self, data):
|
||||
return bpy.data.images.new(
|
||||
name=data['name'],
|
||||
@ -84,28 +65,23 @@ class BlImage(BlDatablock):
|
||||
)
|
||||
|
||||
def _load(self, data, target):
|
||||
image = target
|
||||
prefs = utils.get_preferences()
|
||||
img_format = data['file_format']
|
||||
img_name = f"{self.uuid}.{format_to_ext[img_format]}"
|
||||
|
||||
img_path = os.path.join(prefs.cache_directory, img_name)
|
||||
os.makedirs(prefs.cache_directory, exist_ok=True)
|
||||
file = open(img_path, 'wb')
|
||||
file.write(data["pixels"])
|
||||
file.close()
|
||||
|
||||
image.source = 'FILE'
|
||||
image.filepath = img_path
|
||||
image.colorspace_settings.name = data["colorspace_settings"]["name"]
|
||||
|
||||
loader = Loader()
|
||||
loader.load(data, target)
|
||||
|
||||
target.source = 'FILE'
|
||||
target.filepath_raw = get_filepath(data['filename'])
|
||||
target.colorspace_settings.name = data["colorspace_settings"]["name"]
|
||||
|
||||
|
||||
def _dump(self, instance=None):
|
||||
assert(instance)
|
||||
data = {}
|
||||
data['pixels'] = self.dump_image(instance)
|
||||
|
||||
filename = Path(instance.filepath).name
|
||||
|
||||
data = {
|
||||
"filename": filename
|
||||
}
|
||||
|
||||
dumper = Dumper()
|
||||
dumper.depth = 2
|
||||
dumper.include_filter = [
|
||||
@ -114,13 +90,9 @@ class BlImage(BlDatablock):
|
||||
'height',
|
||||
'alpha',
|
||||
'float_buffer',
|
||||
'file_format',
|
||||
'alpha_mode',
|
||||
'filepath',
|
||||
'source',
|
||||
'colorspace_settings']
|
||||
data.update(dumper.dump(instance))
|
||||
|
||||
return data
|
||||
|
||||
def diff(self):
|
||||
@ -128,3 +100,24 @@ class BlImage(BlDatablock):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _resolve_deps_implementation(self):
|
||||
deps = []
|
||||
if self.instance.filepath:
|
||||
|
||||
if self.instance.packed_file:
|
||||
filename = Path(bpy.path.abspath(self.instance.filepath)).name
|
||||
self.instance.filepath_raw = get_filepath(filename)
|
||||
self.instance.save()
|
||||
# An image can't be unpacked to the modified path
|
||||
# TODO: make a bug report
|
||||
self.instance.unpack(method="REMOVE")
|
||||
|
||||
elif self.instance.source == "GENERATED":
|
||||
filename = f"{self.instance.name}.png"
|
||||
self.instance.filepath = get_filepath(filename)
|
||||
self.instance.save()
|
||||
|
||||
deps.append(Path(bpy.path.abspath(self.instance.filepath)))
|
||||
|
||||
return deps
|
||||
|
@ -21,12 +21,12 @@ import mathutils
|
||||
import logging
|
||||
import re
|
||||
|
||||
from ..utils import get_datablock_from_uuid
|
||||
from .dump_anything import Loader, Dumper
|
||||
from .bl_datablock import BlDatablock
|
||||
from .bl_datablock import BlDatablock, get_datablock_from_uuid
|
||||
|
||||
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
||||
|
||||
|
||||
def load_node(node_data, node_tree):
|
||||
""" Load a node into a node_tree from a dict
|
||||
|
||||
@ -42,7 +42,7 @@ def load_node(node_data, node_tree):
|
||||
image_uuid = node_data.get('image_uuid', None)
|
||||
|
||||
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"]:
|
||||
if hasattr(target_node.inputs[input], "default_value"):
|
||||
@ -52,6 +52,14 @@ def load_node(node_data, node_tree):
|
||||
logging.error(
|
||||
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):
|
||||
""" Load node_tree links from a list
|
||||
@ -63,8 +71,10 @@ def load_links(links_data, node_tree):
|
||||
"""
|
||||
|
||||
for link in links_data:
|
||||
input_socket = node_tree.nodes[link['to_node']].inputs[int(link['to_socket'])]
|
||||
output_socket = node_tree.nodes[link['from_node']].outputs[int(link['from_socket'])]
|
||||
input_socket = node_tree.nodes[link['to_node']
|
||||
].inputs[int(link['to_socket'])]
|
||||
output_socket = node_tree.nodes[link['from_node']].outputs[int(
|
||||
link['from_socket'])]
|
||||
node_tree.links.new(input_socket, output_socket)
|
||||
|
||||
|
||||
@ -79,8 +89,10 @@ def dump_links(links):
|
||||
links_data = []
|
||||
|
||||
for link in links:
|
||||
to_socket = NODE_SOCKET_INDEX.search(link.to_socket.path_from_id()).group(1)
|
||||
from_socket = NODE_SOCKET_INDEX.search(link.from_socket.path_from_id()).group(1)
|
||||
to_socket = NODE_SOCKET_INDEX.search(
|
||||
link.to_socket.path_from_id()).group(1)
|
||||
from_socket = NODE_SOCKET_INDEX.search(
|
||||
link.from_socket.path_from_id()).group(1)
|
||||
links_data.append({
|
||||
'to_node': link.to_node.name,
|
||||
'to_socket': to_socket,
|
||||
@ -106,6 +118,7 @@ def dump_node(node):
|
||||
"show_expanded",
|
||||
"name_full",
|
||||
"select",
|
||||
"bl_label",
|
||||
"bl_height_min",
|
||||
"bl_height_max",
|
||||
"bl_height_default",
|
||||
@ -137,8 +150,17 @@ def dump_node(node):
|
||||
input_dumper.include_filter = ["default_value"]
|
||||
|
||||
if hasattr(i, 'default_value'):
|
||||
dumped_node['inputs'][i.name] = input_dumper.dump(
|
||||
i)
|
||||
dumped_node['inputs'][i.name] = input_dumper.dump(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'):
|
||||
ramp_dumper = Dumper()
|
||||
ramp_dumper.depth = 4
|
||||
@ -163,6 +185,12 @@ def dump_node(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):
|
||||
bl_id = "materials"
|
||||
bl_class = bpy.types.Material
|
||||
@ -177,22 +205,22 @@ class BlMaterial(BlDatablock):
|
||||
|
||||
def _load_implementation(self, data, target):
|
||||
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:
|
||||
bpy.data.materials.create_gpencil_data(target)
|
||||
|
||||
loader.load(
|
||||
target.grease_pencil, data['grease_pencil'])
|
||||
|
||||
if data["use_nodes"]:
|
||||
loader.load(target.grease_pencil, data['grease_pencil'])
|
||||
elif use_nodes:
|
||||
if target.node_tree is None:
|
||||
target.use_nodes = True
|
||||
|
||||
target.node_tree.nodes.clear()
|
||||
|
||||
loader.load(target, data)
|
||||
|
||||
# Load nodes
|
||||
for node in data["node_tree"]["nodes"]:
|
||||
load_node(data["node_tree"]["nodes"][node], target.node_tree)
|
||||
@ -206,57 +234,69 @@ class BlMaterial(BlDatablock):
|
||||
assert(instance)
|
||||
mat_dumper = Dumper()
|
||||
mat_dumper.depth = 2
|
||||
mat_dumper.exclude_filter = [
|
||||
"is_embed_data",
|
||||
"is_evaluated",
|
||||
"name_full",
|
||||
"bl_description",
|
||||
"bl_icon",
|
||||
"bl_idname",
|
||||
"bl_label",
|
||||
"preview",
|
||||
"original",
|
||||
"uuid",
|
||||
"users",
|
||||
"alpha_threshold",
|
||||
"line_color",
|
||||
"view_center",
|
||||
mat_dumper.include_filter = [
|
||||
'name',
|
||||
'blend_method',
|
||||
'shadow_method',
|
||||
'alpha_threshold',
|
||||
'show_transparent_back',
|
||||
'use_backface_culling',
|
||||
'use_screen_refraction',
|
||||
'use_sss_translucency',
|
||||
'refraction_depth',
|
||||
'preview_render_type',
|
||||
'use_preview_world',
|
||||
'pass_index',
|
||||
'use_nodes',
|
||||
'diffuse_color',
|
||||
'specular_color',
|
||||
'roughness',
|
||||
'specular_intensity',
|
||||
'metallic',
|
||||
'line_color',
|
||||
'line_priority',
|
||||
'is_grease_pencil'
|
||||
]
|
||||
data = mat_dumper.dump(instance)
|
||||
|
||||
if 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)
|
||||
|
||||
if instance.is_grease_pencil:
|
||||
elif instance.is_grease_pencil:
|
||||
gp_mat_dumper = Dumper()
|
||||
gp_mat_dumper.depth = 3
|
||||
|
||||
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_fill',
|
||||
'alignment_mode',
|
||||
'pass_index',
|
||||
'mode',
|
||||
'stroke_style',
|
||||
'color',
|
||||
'use_overlap_strokes',
|
||||
'show_fill',
|
||||
# 'stroke_image',
|
||||
'fill_style',
|
||||
'fill_color',
|
||||
'pass_index',
|
||||
'alignment_mode',
|
||||
# 'fill_image',
|
||||
'texture_opacity',
|
||||
'mix_factor',
|
||||
'texture_offset',
|
||||
'texture_angle',
|
||||
'texture_scale',
|
||||
'texture_clamp',
|
||||
'gradient_type',
|
||||
'mix_color',
|
||||
'flip'
|
||||
# 'fill_image',
|
||||
]
|
||||
data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil)
|
||||
return data
|
||||
@ -266,9 +306,7 @@ class BlMaterial(BlDatablock):
|
||||
deps = []
|
||||
|
||||
if self.instance.use_nodes:
|
||||
for node in self.instance.node_tree.nodes:
|
||||
if node.type in ['TEX_IMAGE','TEX_ENVIRONMENT']:
|
||||
deps.append(node.image)
|
||||
deps.extend(get_node_tree_dependencies(self.instance.node_tree))
|
||||
if self.is_library:
|
||||
deps.append(self.instance.library)
|
||||
|
||||
|
@ -89,24 +89,26 @@ class BlMesh(BlDatablock):
|
||||
np_load_collection(data["polygons"],target.polygons, POLYGON)
|
||||
|
||||
# UV Layers
|
||||
for layer in data['uv_layers']:
|
||||
if layer not in target.uv_layers:
|
||||
target.uv_layers.new(name=layer)
|
||||
if 'uv_layers' in data.keys():
|
||||
for layer in data['uv_layers']:
|
||||
if layer not in target.uv_layers:
|
||||
target.uv_layers.new(name=layer)
|
||||
|
||||
np_load_collection_primitives(
|
||||
target.uv_layers[layer].data,
|
||||
'uv',
|
||||
data["uv_layers"][layer]['data'])
|
||||
np_load_collection_primitives(
|
||||
target.uv_layers[layer].data,
|
||||
'uv',
|
||||
data["uv_layers"][layer]['data'])
|
||||
|
||||
# Vertex color
|
||||
for color_layer in data['vertex_colors']:
|
||||
if color_layer not in target.vertex_colors:
|
||||
target.vertex_colors.new(name=color_layer)
|
||||
if 'vertex_colors' in data.keys():
|
||||
for color_layer in data['vertex_colors']:
|
||||
if color_layer not in target.vertex_colors:
|
||||
target.vertex_colors.new(name=color_layer)
|
||||
|
||||
np_load_collection_primitives(
|
||||
target.vertex_colors[color_layer].data,
|
||||
'color',
|
||||
data["vertex_colors"][color_layer]['data'])
|
||||
np_load_collection_primitives(
|
||||
target.vertex_colors[color_layer].data,
|
||||
'color',
|
||||
data["vertex_colors"][color_layer]['data'])
|
||||
|
||||
target.validate()
|
||||
target.update()
|
||||
@ -114,7 +116,7 @@ class BlMesh(BlDatablock):
|
||||
def _dump_implementation(self, data, instance=None):
|
||||
assert(instance)
|
||||
|
||||
if instance.is_editmode and not self.preferences.enable_editmode_updates:
|
||||
if instance.is_editmode and not self.preferences.sync_flags.sync_during_editmode:
|
||||
raise ContextError("Mesh is in edit mode")
|
||||
mesh = instance
|
||||
|
||||
@ -147,16 +149,18 @@ class BlMesh(BlDatablock):
|
||||
data["loops"] = np_dump_collection(mesh.loops, LOOP)
|
||||
|
||||
# UV Layers
|
||||
data['uv_layers'] = {}
|
||||
for layer in mesh.uv_layers:
|
||||
data['uv_layers'][layer.name] = {}
|
||||
data['uv_layers'][layer.name]['data'] = np_dump_collection_primitive(layer.data, 'uv')
|
||||
if mesh.uv_layers:
|
||||
data['uv_layers'] = {}
|
||||
for layer in mesh.uv_layers:
|
||||
data['uv_layers'][layer.name] = {}
|
||||
data['uv_layers'][layer.name]['data'] = np_dump_collection_primitive(layer.data, 'uv')
|
||||
|
||||
# Vertex color
|
||||
data['vertex_colors'] = {}
|
||||
for color_map in mesh.vertex_colors:
|
||||
data['vertex_colors'][color_map.name] = {}
|
||||
data['vertex_colors'][color_map.name]['data'] = np_dump_collection_primitive(color_map.data, 'color')
|
||||
if mesh.vertex_colors:
|
||||
data['vertex_colors'] = {}
|
||||
for color_map in mesh.vertex_colors:
|
||||
data['vertex_colors'][color_map.name] = {}
|
||||
data['vertex_colors'][color_map.name]['data'] = np_dump_collection_primitive(color_map.data, 'color')
|
||||
|
||||
# Fix material index
|
||||
m_list = []
|
||||
|
@ -22,8 +22,7 @@ import bpy
|
||||
import mathutils
|
||||
from replication.exception import ContextError
|
||||
|
||||
from ..utils import get_datablock_from_uuid
|
||||
from .bl_datablock import BlDatablock
|
||||
from .bl_datablock import BlDatablock, get_datablock_from_uuid
|
||||
from .dump_anything import Dumper, Loader
|
||||
from replication.exception import ReparentException
|
||||
|
||||
@ -161,6 +160,8 @@ class BlObject(BlDatablock):
|
||||
# Load transformation data
|
||||
loader.load(target, data)
|
||||
|
||||
loader.load(target.display, data['display'])
|
||||
|
||||
# Pose
|
||||
if 'pose' in data:
|
||||
if not target.pose:
|
||||
@ -199,7 +200,7 @@ class BlObject(BlDatablock):
|
||||
assert(instance)
|
||||
|
||||
if _is_editmode(instance):
|
||||
if self.preferences.enable_editmode_updates:
|
||||
if self.preferences.sync_flags.sync_during_editmode:
|
||||
instance.update_from_editmode()
|
||||
else:
|
||||
raise ContextError("Object is in edit-mode.")
|
||||
@ -230,11 +231,27 @@ class BlObject(BlDatablock):
|
||||
'lock_location',
|
||||
'lock_rotation',
|
||||
'lock_scale',
|
||||
'hide_render',
|
||||
'display_type',
|
||||
'display_bounds_type',
|
||||
'show_bounds',
|
||||
'show_name',
|
||||
'show_axis',
|
||||
'show_wire',
|
||||
'show_all_edges',
|
||||
'show_texture_space',
|
||||
'show_in_front',
|
||||
'type',
|
||||
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
|
||||
]
|
||||
|
||||
data = dumper.dump(instance)
|
||||
|
||||
dumper.include_filter = [
|
||||
'show_shadows',
|
||||
]
|
||||
data['display'] = dumper.dump(instance.display)
|
||||
|
||||
data['data_uuid'] = getattr(instance.data, 'uuid', None)
|
||||
if self.is_library:
|
||||
return data
|
||||
|
@ -22,7 +22,244 @@ import mathutils
|
||||
from .dump_anything import Loader, Dumper
|
||||
from .bl_datablock import BlDatablock
|
||||
from .bl_collection import dump_collection_children, dump_collection_objects, load_collection_childrens, load_collection_objects
|
||||
from ..utils import get_preferences
|
||||
from replication.constants import (DIFF_JSON, MODIFIED)
|
||||
from deepdiff import DeepDiff
|
||||
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):
|
||||
bl_id = "scenes"
|
||||
@ -33,6 +270,11 @@ class BlScene(BlDatablock):
|
||||
bl_check_common = True
|
||||
bl_icon = 'SCENE_DATA'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.diff_method = DIFF_JSON
|
||||
|
||||
def _construct(self, data):
|
||||
instance = bpy.data.scenes.new(data["name"])
|
||||
return instance
|
||||
@ -43,32 +285,38 @@ class BlScene(BlDatablock):
|
||||
loader.load(target, data)
|
||||
|
||||
# Load master collection
|
||||
load_collection_objects(data['collection']['objects'], target.collection)
|
||||
load_collection_childrens(data['collection']['children'], target.collection)
|
||||
load_collection_objects(
|
||||
data['collection']['objects'], target.collection)
|
||||
load_collection_childrens(
|
||||
data['collection']['children'], target.collection)
|
||||
|
||||
if 'world' in data.keys():
|
||||
target.world = bpy.data.worlds[data['world']]
|
||||
|
||||
|
||||
# Annotation
|
||||
if 'grease_pencil' in data.keys():
|
||||
target.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']]
|
||||
|
||||
if 'eevee' in data.keys():
|
||||
loader.load(target.eevee, data['eevee'])
|
||||
|
||||
if 'cycles' in data.keys():
|
||||
loader.load(target.eevee, data['cycles'])
|
||||
if self.preferences.sync_flags.sync_render_settings:
|
||||
if 'eevee' in data.keys():
|
||||
loader.load(target.eevee, data['eevee'])
|
||||
|
||||
if 'render' in data.keys():
|
||||
loader.load(target.render, data['render'])
|
||||
if 'cycles' in data.keys():
|
||||
loader.load(target.cycles, data['cycles'])
|
||||
|
||||
if 'view_settings' in data.keys():
|
||||
loader.load(target.view_settings, data['view_settings'])
|
||||
if target.view_settings.use_curve_mapping:
|
||||
#TODO: change this ugly fix
|
||||
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()
|
||||
if 'render' in data.keys():
|
||||
loader.load(target.render, data['render'])
|
||||
|
||||
if 'view_settings' in data.keys():
|
||||
loader.load(target.view_settings, data['view_settings'])
|
||||
if target.view_settings.use_curve_mapping and \
|
||||
'curve_mapping' in data['view_settings']:
|
||||
# TODO: change this ugly fix
|
||||
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()
|
||||
|
||||
def _dump_implementation(self, data, instance=None):
|
||||
assert(instance)
|
||||
@ -80,58 +328,55 @@ class BlScene(BlDatablock):
|
||||
'name',
|
||||
'world',
|
||||
'id',
|
||||
'camera',
|
||||
'grease_pencil',
|
||||
'frame_start',
|
||||
'frame_end',
|
||||
'frame_step',
|
||||
]
|
||||
if self.preferences.sync_flags.sync_active_camera:
|
||||
scene_dumper.include_filter.append('camera')
|
||||
|
||||
data = scene_dumper.dump(instance)
|
||||
|
||||
scene_dumper.depth = 3
|
||||
|
||||
scene_dumper.include_filter = ['children','objects','name']
|
||||
scene_dumper.include_filter = ['children', 'objects', 'name']
|
||||
data['collection'] = {}
|
||||
data['collection']['children'] = dump_collection_children(instance.collection)
|
||||
data['collection']['objects'] = dump_collection_objects(instance.collection)
|
||||
|
||||
data['collection']['children'] = dump_collection_children(
|
||||
instance.collection)
|
||||
data['collection']['objects'] = dump_collection_objects(
|
||||
instance.collection)
|
||||
|
||||
scene_dumper.depth = 1
|
||||
scene_dumper.include_filter = None
|
||||
|
||||
pref = get_preferences()
|
||||
|
||||
if pref.sync_flags.sync_render_settings:
|
||||
scene_dumper.exclude_filter = [
|
||||
'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'
|
||||
]
|
||||
data['eevee'] = scene_dumper.dump(instance.eevee)
|
||||
data['cycles'] = scene_dumper.dump(instance.cycles)
|
||||
data['view_settings'] = scene_dumper.dump(instance.view_settings)
|
||||
if self.preferences.sync_flags.sync_render_settings:
|
||||
scene_dumper.include_filter = RENDER_SETTINGS
|
||||
|
||||
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:
|
||||
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.include_filter = [
|
||||
'curves',
|
||||
'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
|
||||
|
||||
def _resolve_deps_implementation(self):
|
||||
@ -140,17 +385,31 @@ class BlScene(BlDatablock):
|
||||
# child collections
|
||||
for child in self.instance.collection.children:
|
||||
deps.append(child)
|
||||
|
||||
|
||||
# childs objects
|
||||
for object in self.instance.objects:
|
||||
for object in self.instance.collection.objects:
|
||||
deps.append(object)
|
||||
|
||||
|
||||
# world
|
||||
if self.instance.world:
|
||||
deps.append(self.instance.world)
|
||||
|
||||
|
||||
# annotations
|
||||
if self.instance.grease_pencil:
|
||||
deps.append(self.instance.grease_pencil)
|
||||
|
||||
return deps
|
||||
|
||||
def diff(self):
|
||||
exclude_path = []
|
||||
|
||||
if not self.preferences.sync_flags.sync_render_settings:
|
||||
exclude_path.append("root['eevee']")
|
||||
exclude_path.append("root['cycles']")
|
||||
exclude_path.append("root['view_settings']")
|
||||
exclude_path.append("root['render']")
|
||||
|
||||
if not self.preferences.sync_flags.sync_active_camera:
|
||||
exclude_path.append("root['camera']")
|
||||
|
||||
return DeepDiff(self.data, self._dump(instance=self.instance), exclude_paths=exclude_path)
|
||||
|
@ -16,14 +16,16 @@
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
import os
|
||||
import logging
|
||||
import pathlib
|
||||
from .. import utils
|
||||
from .dump_anything import Loader, Dumper
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from .bl_file import get_filepath, ensure_unpacked
|
||||
from .bl_datablock import BlDatablock
|
||||
from .dump_anything import Dumper, Loader
|
||||
|
||||
|
||||
class BlSound(BlDatablock):
|
||||
bl_id = "sounds"
|
||||
@ -35,40 +37,33 @@ class BlSound(BlDatablock):
|
||||
bl_icon = 'SOUND'
|
||||
|
||||
def _construct(self, data):
|
||||
if 'file' in data.keys():
|
||||
prefs = utils.get_preferences()
|
||||
ext = data['filepath'].split(".")[-1]
|
||||
sound_name = f"{self.uuid}.{ext}"
|
||||
sound_path = os.path.join(prefs.cache_directory, sound_name)
|
||||
|
||||
os.makedirs(prefs.cache_directory, exist_ok=True)
|
||||
file = open(sound_path, 'wb')
|
||||
file.write(data["file"])
|
||||
file.close()
|
||||
filename = data.get('filename')
|
||||
|
||||
logging.info(f'loading {sound_path}')
|
||||
return bpy.data.sounds.load(sound_path)
|
||||
return bpy.data.sounds.load(get_filepath(filename))
|
||||
|
||||
def _load(self, data, target):
|
||||
loader = Loader()
|
||||
loader.load(target, data)
|
||||
|
||||
def _dump(self, instance=None):
|
||||
if not instance.packed_file:
|
||||
# prefs = utils.get_preferences()
|
||||
# ext = pathlib.Path(instance.filepath).suffix
|
||||
# sound_name = f"{self.uuid}{ext}"
|
||||
# sound_path = os.path.join(prefs.cache_directory, sound_name)
|
||||
# instance.filepath = sound_path
|
||||
instance.pack()
|
||||
#TODO:use file locally with unpack(method='USE_ORIGINAL') ?
|
||||
|
||||
return {
|
||||
'filepath':instance.filepath,
|
||||
'name':instance.name,
|
||||
'file': instance.packed_file.data
|
||||
}
|
||||
|
||||
|
||||
def diff(self):
|
||||
return False
|
||||
|
||||
def _dump(self, instance=None):
|
||||
filename = Path(instance.filepath).name
|
||||
|
||||
if not filename:
|
||||
raise FileExistsError(instance.filepath)
|
||||
|
||||
return {
|
||||
'filename': filename,
|
||||
'name': instance.name
|
||||
}
|
||||
|
||||
def _resolve_deps_implementation(self):
|
||||
deps = []
|
||||
if self.instance.filepath and self.instance.filepath != '<builtin>':
|
||||
ensure_unpacked(self.instance)
|
||||
|
||||
deps.append(Path(bpy.path.abspath(self.instance.filepath)))
|
||||
|
||||
return deps
|
||||
|
@ -21,7 +21,11 @@ import mathutils
|
||||
|
||||
from .dump_anything import Loader, Dumper
|
||||
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):
|
||||
@ -37,6 +41,9 @@ class BlWorld(BlDatablock):
|
||||
return bpy.data.worlds.new(data["name"])
|
||||
|
||||
def _load_implementation(self, data, target):
|
||||
loader = Loader()
|
||||
loader.load(target, data)
|
||||
|
||||
if data["use_nodes"]:
|
||||
if target.node_tree is None:
|
||||
target.use_nodes = True
|
||||
@ -49,7 +56,6 @@ class BlWorld(BlDatablock):
|
||||
# Load nodes links
|
||||
target.node_tree.links.clear()
|
||||
|
||||
|
||||
load_links(data["node_tree"]["links"], target.node_tree)
|
||||
|
||||
def _dump_implementation(self, data, instance=None):
|
||||
@ -60,6 +66,7 @@ class BlWorld(BlDatablock):
|
||||
world_dumper.include_filter = [
|
||||
"use_nodes",
|
||||
"name",
|
||||
"color"
|
||||
]
|
||||
data = world_dumper.dump(instance)
|
||||
if instance.use_nodes:
|
||||
@ -79,10 +86,7 @@ class BlWorld(BlDatablock):
|
||||
deps = []
|
||||
|
||||
if self.instance.use_nodes:
|
||||
for node in self.instance.node_tree.nodes:
|
||||
if node.type in ['TEX_IMAGE','TEX_ENVIRONMENT']:
|
||||
deps.append(node.image)
|
||||
deps.extend(get_node_tree_dependencies(self.instance.node_tree))
|
||||
if self.is_library:
|
||||
deps.append(self.instance.library)
|
||||
return deps
|
||||
|
||||
|
@ -24,8 +24,8 @@ import numpy as np
|
||||
|
||||
|
||||
BPY_TO_NUMPY_TYPES = {
|
||||
'FLOAT': np.float,
|
||||
'INT': np.int,
|
||||
'FLOAT': np.float32,
|
||||
'INT': np.int32,
|
||||
'BOOL': np.bool}
|
||||
|
||||
PRIMITIVE_TYPES = ['FLOAT', 'INT', 'BOOLEAN']
|
||||
@ -47,7 +47,7 @@ def np_load_collection(dikt: dict, collection: bpy.types.CollectionProperty, att
|
||||
:type attributes: list
|
||||
"""
|
||||
if not dikt or len(collection) == 0:
|
||||
logging.warning(f'Skipping collection')
|
||||
logging.debug(f'Skipping collection {collection}')
|
||||
return
|
||||
|
||||
if attributes is None:
|
||||
@ -626,11 +626,11 @@ class Loader:
|
||||
for k in self._ordered_keys(dump.keys()):
|
||||
v = dump[k]
|
||||
if not hasattr(default.read(), k):
|
||||
logging.debug(f"Load default, skipping {default} : {k}")
|
||||
continue
|
||||
try:
|
||||
self._load_any(default.extend(k), v)
|
||||
except Exception as err:
|
||||
logging.debug(f"Cannot load {k}: {err}")
|
||||
logging.debug(f"Skipping {k}")
|
||||
|
||||
@property
|
||||
def match_subset_all(self):
|
||||
|
@ -19,7 +19,15 @@ import logging
|
||||
|
||||
import bpy
|
||||
|
||||
from . import operators, 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,
|
||||
UP,
|
||||
RP_COMMON,
|
||||
@ -31,11 +39,16 @@ from replication.constants import (FETCHED,
|
||||
STATE_SRV_SYNC,
|
||||
REPARENT)
|
||||
|
||||
from replication.interface import session
|
||||
from replication.exception import NonAuthorizedOperationError
|
||||
|
||||
class Delayable():
|
||||
"""Delayable task interface
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_registered = False
|
||||
|
||||
def register(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -53,13 +66,21 @@ class Timer(Delayable):
|
||||
"""
|
||||
|
||||
def __init__(self, duration=1):
|
||||
super().__init__()
|
||||
self._timeout = duration
|
||||
self._running = True
|
||||
|
||||
def register(self):
|
||||
"""Register the timer into the blender timer system
|
||||
"""
|
||||
bpy.app.timers.register(self.main)
|
||||
|
||||
if not self.is_registered:
|
||||
bpy.app.timers.register(self.main)
|
||||
self.is_registered = True
|
||||
logging.debug(f"Register {self.__class__.__name__}")
|
||||
else:
|
||||
logging.debug(
|
||||
f"Timer {self.__class__.__name__} already registered")
|
||||
|
||||
def main(self):
|
||||
self.execute()
|
||||
@ -87,29 +108,28 @@ class ApplyTimer(Timer):
|
||||
super().__init__(timout)
|
||||
|
||||
def execute(self):
|
||||
client = operators.client
|
||||
if client and client.state['STATE'] == STATE_ACTIVE:
|
||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||
if self._type:
|
||||
nodes = client.list(filter=self._type)
|
||||
nodes = session.list(filter=self._type)
|
||||
else:
|
||||
nodes = client.list()
|
||||
nodes = session.list()
|
||||
|
||||
for node in nodes:
|
||||
node_ref = client.get(uuid=node)
|
||||
node_ref = session.get(uuid=node)
|
||||
|
||||
if node_ref.state == FETCHED:
|
||||
try:
|
||||
client.apply(node, force=True)
|
||||
session.apply(node)
|
||||
except Exception as e:
|
||||
logging.error(f"Fail to apply {node_ref.uuid}: {e}")
|
||||
elif node_ref.state == REPARENT:
|
||||
# Reload the node
|
||||
node_ref.remove_instance()
|
||||
node_ref.resolve()
|
||||
client.apply(node, force=True)
|
||||
for parent in client._graph.find_parents(node):
|
||||
session.apply(node)
|
||||
for parent in session._graph.find_parents(node):
|
||||
logging.info(f"Applying parent {parent}")
|
||||
client.apply(parent, force=True)
|
||||
session.apply(parent, force=True)
|
||||
node_ref.state = UP
|
||||
|
||||
|
||||
@ -121,7 +141,6 @@ class DynamicRightSelectTimer(Timer):
|
||||
self._right_strategy = RP_COMMON
|
||||
|
||||
def execute(self):
|
||||
session = operators.client
|
||||
settings = utils.get_preferences()
|
||||
|
||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||
@ -148,10 +167,13 @@ class DynamicRightSelectTimer(Timer):
|
||||
recursive = True
|
||||
if node.data and 'instance_type' in node.data.keys():
|
||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
RP_COMMON,
|
||||
recursive=recursive)
|
||||
try:
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
RP_COMMON,
|
||||
recursive=recursive)
|
||||
except NonAuthorizedOperationError:
|
||||
logging.warning(f"Not authorized to change {node} owner")
|
||||
|
||||
# change new selection to our
|
||||
for obj in obj_ours:
|
||||
@ -162,10 +184,13 @@ class DynamicRightSelectTimer(Timer):
|
||||
if node.data and 'instance_type' in node.data.keys():
|
||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
||||
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
settings.username,
|
||||
recursive=recursive)
|
||||
try:
|
||||
session.change_owner(
|
||||
node.uuid,
|
||||
settings.username,
|
||||
recursive=recursive)
|
||||
except NonAuthorizedOperationError:
|
||||
logging.warning(f"Not authorized to change {node} owner")
|
||||
else:
|
||||
return
|
||||
|
||||
@ -184,73 +209,20 @@ class DynamicRightSelectTimer(Timer):
|
||||
filter_owner=settings.username)
|
||||
for key in owned_keys:
|
||||
node = session.get(uuid=key)
|
||||
try:
|
||||
session.change_owner(
|
||||
key,
|
||||
RP_COMMON,
|
||||
recursive=recursive)
|
||||
except NonAuthorizedOperationError:
|
||||
logging.warning(f"Not authorized to change {key} owner")
|
||||
|
||||
session.change_owner(
|
||||
key,
|
||||
RP_COMMON,
|
||||
recursive=recursive)
|
||||
|
||||
for user, user_info in session.online_users.items():
|
||||
if user != settings.username:
|
||||
metadata = user_info.get('metadata')
|
||||
|
||||
if 'selected_objects' in metadata:
|
||||
# Update selectionnable objects
|
||||
for obj in bpy.data.objects:
|
||||
if obj.hide_select and obj.uuid not in 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):
|
||||
self._handler = None
|
||||
|
||||
def register(self):
|
||||
self._handler = bpy.types.SpaceView3D.draw_handler_add(
|
||||
self.execute, (), 'WINDOW', 'POST_VIEW')
|
||||
|
||||
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):
|
||||
session = getattr(operators, 'client', None)
|
||||
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()
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
object_uuid = getattr(obj, 'uuid', None)
|
||||
if object_uuid:
|
||||
is_selectable = not session.is_readonly(object_uuid)
|
||||
if obj.hide_select != is_selectable:
|
||||
obj.hide_select = is_selectable
|
||||
|
||||
class ClientUpdate(Timer):
|
||||
def __init__(self, timout=.1):
|
||||
@ -260,27 +232,25 @@ class ClientUpdate(Timer):
|
||||
|
||||
def execute(self):
|
||||
settings = utils.get_preferences()
|
||||
session = getattr(operators, 'client', None)
|
||||
renderer = getattr(presence, 'renderer', None)
|
||||
|
||||
if session and renderer:
|
||||
if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]:
|
||||
local_user = operators.client.online_users.get(
|
||||
local_user = session.online_users.get(
|
||||
settings.username)
|
||||
|
||||
if not local_user:
|
||||
return
|
||||
else:
|
||||
for username, user_data in operators.client.online_users.items():
|
||||
for username, user_data in session.online_users.items():
|
||||
if username != settings.username:
|
||||
cached_user_data = self.users_metadata.get(
|
||||
username)
|
||||
new_user_data = operators.client.online_users[username]['metadata']
|
||||
new_user_data = session.online_users[username]['metadata']
|
||||
|
||||
if cached_user_data is None:
|
||||
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']:
|
||||
presence.refresh_3d_view()
|
||||
refresh_3d_view()
|
||||
self.users_metadata[username] = user_data['metadata']
|
||||
break
|
||||
else:
|
||||
@ -289,13 +259,13 @@ class ClientUpdate(Timer):
|
||||
local_user_metadata = local_user.get('metadata')
|
||||
scene_current = bpy.context.scene.name
|
||||
local_user = session.online_users.get(settings.username)
|
||||
current_view_corners = presence.get_view_corners()
|
||||
current_view_corners = generate_user_camera()
|
||||
|
||||
# Init client metadata
|
||||
if not local_user_metadata or 'color' not in local_user_metadata.keys():
|
||||
metadata = {
|
||||
'view_corners': presence.get_view_matrix(),
|
||||
'view_matrix': presence.get_view_matrix(),
|
||||
'view_corners': get_view_matrix(),
|
||||
'view_matrix': get_view_matrix(),
|
||||
'color': (settings.client_color.r,
|
||||
settings.client_color.g,
|
||||
settings.client_color.b,
|
||||
@ -312,7 +282,7 @@ class ClientUpdate(Timer):
|
||||
session.update_user_metadata(local_user_metadata)
|
||||
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']:
|
||||
local_user_metadata['view_corners'] = current_view_corners
|
||||
local_user_metadata['view_matrix'] = presence.get_view_matrix(
|
||||
local_user_metadata['view_matrix'] = get_view_matrix(
|
||||
)
|
||||
session.update_user_metadata(local_user_metadata)
|
||||
|
||||
@ -322,27 +292,27 @@ class SessionStatusUpdate(Timer):
|
||||
super().__init__(timout)
|
||||
|
||||
def execute(self):
|
||||
presence.refresh_sidebar_view()
|
||||
refresh_sidebar_view()
|
||||
|
||||
|
||||
class SessionUserSync(Timer):
|
||||
def __init__(self, timout=1):
|
||||
super().__init__(timout)
|
||||
self.settings = utils.get_preferences()
|
||||
|
||||
def execute(self):
|
||||
session = getattr(operators, 'client', None)
|
||||
renderer = getattr(presence, 'renderer', None)
|
||||
|
||||
if session and renderer:
|
||||
# sync online users
|
||||
session_users = operators.client.online_users
|
||||
session_users = session.online_users
|
||||
ui_users = bpy.context.window_manager.online_users
|
||||
|
||||
for index, user in enumerate(ui_users):
|
||||
if user.username not in session_users.keys():
|
||||
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)
|
||||
renderer.flush_selection()
|
||||
renderer.flush_users()
|
||||
break
|
||||
|
||||
for user in session_users:
|
||||
@ -350,3 +320,22 @@ class SessionUserSync(Timer):
|
||||
new_key = ui_users.add()
|
||||
new_key.name = 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):
|
||||
def __init__(self, timout=1, execution_queue=None):
|
||||
super().__init__(timout)
|
||||
self.execution_queue = execution_queue
|
||||
|
||||
def execute(self):
|
||||
while not self.execution_queue.empty():
|
||||
function = self.execution_queue.get()
|
||||
logging.debug(f"Executing {function.__name__}")
|
||||
function()
|
||||
|
@ -64,7 +64,7 @@ def install_package(name, version):
|
||||
|
||||
def check_package_version(name, required_version):
|
||||
logging.info(f"Checking {name} version...")
|
||||
out = subprocess.run(f"{str(PYTHON_PATH)} -m pip show {name}", capture_output=True)
|
||||
out = subprocess.run([str(PYTHON_PATH), "-m", "pip", "show", name], capture_output=True)
|
||||
|
||||
version = VERSION_EXPR.search(out.stdout.decode())
|
||||
if version and version.group() == required_version:
|
||||
|
@ -21,34 +21,101 @@ import logging
|
||||
import os
|
||||
import queue
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import time
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, Popen, TimeoutExpired
|
||||
import zmq
|
||||
from queue import Queue
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from . import bl_types, delayable, environment, presence, ui, utils
|
||||
from replication.constants import (FETCHED, STATE_ACTIVE,
|
||||
STATE_INITIAL,
|
||||
STATE_SYNCING, RP_COMMON, UP)
|
||||
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
|
||||
STATE_INITIAL, STATE_SYNCING, UP)
|
||||
from replication.data import ReplicatedDataFactory
|
||||
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)
|
||||
|
||||
client = None
|
||||
background_execution_queue = Queue()
|
||||
delayables = []
|
||||
stop_modal_executor = False
|
||||
|
||||
|
||||
def session_callback(name):
|
||||
""" Session callback wrapper
|
||||
|
||||
This allow to encapsulate session callbacks to background_execution_queue.
|
||||
By doing this way callback are executed from the main thread.
|
||||
"""
|
||||
def func_wrapper(func):
|
||||
@session.register(name)
|
||||
def add_background_task():
|
||||
background_execution_queue.put(func)
|
||||
return add_background_task
|
||||
return func_wrapper
|
||||
|
||||
|
||||
@session_callback('on_connection')
|
||||
def initialize_session():
|
||||
"""Session connection init hander
|
||||
"""
|
||||
settings = utils.get_preferences()
|
||||
runtime_settings = bpy.context.window_manager.session
|
||||
|
||||
# Step 1: Constrect nodes
|
||||
for node in session._graph.list_ordered():
|
||||
node_ref = session.get(node)
|
||||
if node_ref.state == FETCHED:
|
||||
node_ref.resolve()
|
||||
|
||||
# Step 2: Load nodes
|
||||
for node in session._graph.list_ordered():
|
||||
node_ref = session.get(node)
|
||||
if node_ref.state == FETCHED:
|
||||
node_ref.apply()
|
||||
|
||||
# Step 4: Register blender timers
|
||||
for d in delayables:
|
||||
d.register()
|
||||
|
||||
if settings.update_method == 'DEPSGRAPH':
|
||||
bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
|
||||
|
||||
bpy.ops.session.apply_armature_operator('INVOKE_DEFAULT')
|
||||
|
||||
|
||||
@session_callback('on_exit')
|
||||
def on_connection_end():
|
||||
"""Session connection finished handler
|
||||
"""
|
||||
global delayables, stop_modal_executor
|
||||
settings = utils.get_preferences()
|
||||
|
||||
# Step 1: Unregister blender timers
|
||||
for d in delayables:
|
||||
try:
|
||||
d.unregister()
|
||||
except:
|
||||
continue
|
||||
|
||||
stop_modal_executor = True
|
||||
|
||||
if settings.update_method == 'DEPSGRAPH':
|
||||
bpy.app.handlers.depsgraph_update_post.remove(
|
||||
depsgraph_evaluation)
|
||||
|
||||
# Step 3: remove file handled
|
||||
logger = logging.getLogger()
|
||||
for handler in logger.handlers:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
logger.removeHandler(handler)
|
||||
|
||||
|
||||
# OPERATORS
|
||||
|
||||
|
||||
class SessionStartOperator(bpy.types.Operator):
|
||||
bl_idname = "session.start"
|
||||
bl_label = "start"
|
||||
@ -61,7 +128,7 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client, delayables
|
||||
global delayables
|
||||
|
||||
settings = utils.get_preferences()
|
||||
runtime_settings = context.window_manager.session
|
||||
@ -70,20 +137,20 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
use_extern_update = settings.update_method == 'DEPSGRAPH'
|
||||
users.clear()
|
||||
delayables.clear()
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
if len(logger.handlers)==1:
|
||||
if len(logger.handlers) == 1:
|
||||
formatter = logging.Formatter(
|
||||
fmt='%(asctime)s CLIENT %(levelname)-8s %(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
|
||||
|
||||
log_directory = os.path.join(
|
||||
settings.cache_directory,
|
||||
"multiuser_client.log")
|
||||
|
||||
os.makedirs(settings.cache_directory, exist_ok=True)
|
||||
|
||||
|
||||
handler = logging.FileHandler(log_directory, mode='w')
|
||||
logger.addHandler(handler)
|
||||
|
||||
@ -104,7 +171,11 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
|
||||
supported_bl_types.append(type_module_class.bl_id)
|
||||
|
||||
# Retreive local replicated types settings
|
||||
if type_impl_name not in settings.supported_datablocks:
|
||||
logging.info(f"{type_impl_name} not found, \
|
||||
regenerate type settings...")
|
||||
settings.generate_supported_types()
|
||||
|
||||
type_local_config = settings.supported_datablocks[type_impl_name]
|
||||
|
||||
bpy_factory.register_type(
|
||||
@ -121,7 +192,7 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
timout=type_local_config.bl_delay_apply,
|
||||
target_type=type_module_class))
|
||||
|
||||
client = Session(
|
||||
session.configure(
|
||||
factory=bpy_factory,
|
||||
python_path=bpy.app.binary_path_python,
|
||||
external_update_handling=use_extern_update)
|
||||
@ -138,11 +209,11 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
runtime_settings.is_host = True
|
||||
runtime_settings.internet_ip = environment.get_ip()
|
||||
|
||||
for scene in bpy.data.scenes:
|
||||
client.add(scene)
|
||||
|
||||
try:
|
||||
client.host(
|
||||
for scene in bpy.data.scenes:
|
||||
session.add(scene)
|
||||
|
||||
session.host(
|
||||
id=settings.username,
|
||||
port=settings.port,
|
||||
ipc_port=settings.ipc_port,
|
||||
@ -160,11 +231,11 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
else:
|
||||
if not runtime_settings.admin:
|
||||
utils.clean_scene()
|
||||
# regular client, no password needed
|
||||
# regular session, no password needed
|
||||
admin_pass = None
|
||||
|
||||
try:
|
||||
client.connect(
|
||||
session.connect(
|
||||
id=settings.username,
|
||||
address=settings.ip,
|
||||
port=settings.port,
|
||||
@ -178,62 +249,22 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
|
||||
# Background client updates service
|
||||
delayables.append(delayable.ClientUpdate())
|
||||
delayables.append(delayable.DrawClient())
|
||||
delayables.append(delayable.DynamicRightSelectTimer())
|
||||
|
||||
session_update = delayable.SessionStatusUpdate()
|
||||
session_user_sync = delayable.SessionUserSync()
|
||||
session_background_executor = delayable.MainThreadExecutor(
|
||||
execution_queue=background_execution_queue)
|
||||
|
||||
session_update.register()
|
||||
session_user_sync.register()
|
||||
session_background_executor.register()
|
||||
|
||||
delayables.append(session_background_executor)
|
||||
delayables.append(session_update)
|
||||
delayables.append(session_user_sync)
|
||||
|
||||
@client.register('on_connection')
|
||||
def initialize_session():
|
||||
settings = utils.get_preferences()
|
||||
|
||||
for node in client._graph.list_ordered():
|
||||
node_ref = client.get(node)
|
||||
if node_ref.state == FETCHED:
|
||||
node_ref.resolve()
|
||||
|
||||
for node in client._graph.list_ordered():
|
||||
node_ref = client.get(node)
|
||||
if node_ref.state == FETCHED:
|
||||
node_ref.apply()
|
||||
|
||||
# Launch drawing module
|
||||
if runtime_settings.enable_presence:
|
||||
presence.renderer.run()
|
||||
|
||||
# Register blender main thread tools
|
||||
for d in delayables:
|
||||
d.register()
|
||||
|
||||
if settings.update_method == 'DEPSGRAPH':
|
||||
bpy.app.handlers.depsgraph_update_post.append(
|
||||
depsgraph_evaluation)
|
||||
|
||||
@client.register('on_exit')
|
||||
def desinitialize_session():
|
||||
global delayables, stop_modal_executor
|
||||
settings = utils.get_preferences()
|
||||
|
||||
for d in delayables:
|
||||
try:
|
||||
d.unregister()
|
||||
except:
|
||||
continue
|
||||
|
||||
stop_modal_executor = True
|
||||
presence.renderer.stop()
|
||||
|
||||
if settings.update_method == 'DEPSGRAPH':
|
||||
bpy.app.handlers.depsgraph_update_post.remove(
|
||||
depsgraph_evaluation)
|
||||
|
||||
bpy.ops.session.apply_armature_operator()
|
||||
|
||||
|
||||
self.report(
|
||||
{'INFO'},
|
||||
@ -269,15 +300,13 @@ class SessionInitOperator(bpy.types.Operator):
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
global client
|
||||
|
||||
if self.init_method == 'EMPTY':
|
||||
utils.clean_scene()
|
||||
|
||||
for scene in bpy.data.scenes:
|
||||
client.add(scene)
|
||||
session.add(scene)
|
||||
|
||||
client.init()
|
||||
session.init()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@ -293,11 +322,12 @@ class SessionStopOperator(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client, delayables, stop_modal_executor
|
||||
global delayables, stop_modal_executor
|
||||
|
||||
if client:
|
||||
if session:
|
||||
try:
|
||||
client.disconnect()
|
||||
session.disconnect()
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
else:
|
||||
@ -309,7 +339,7 @@ class SessionStopOperator(bpy.types.Operator):
|
||||
class SessionKickOperator(bpy.types.Operator):
|
||||
bl_idname = "session.kick"
|
||||
bl_label = "Kick"
|
||||
bl_description = "Kick the user"
|
||||
bl_description = "Kick the target user"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
user: bpy.props.StringProperty()
|
||||
@ -319,11 +349,11 @@ class SessionKickOperator(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client, delayables, stop_modal_executor
|
||||
assert(client)
|
||||
global delayables, stop_modal_executor
|
||||
assert(session)
|
||||
|
||||
try:
|
||||
client.kick(self.user)
|
||||
session.kick(self.user)
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
|
||||
@ -339,8 +369,9 @@ class SessionKickOperator(bpy.types.Operator):
|
||||
|
||||
class SessionPropertyRemoveOperator(bpy.types.Operator):
|
||||
bl_idname = "session.remove_prop"
|
||||
bl_label = "remove"
|
||||
bl_description = "broadcast a property to connected client_instances"
|
||||
bl_label = "Delete cache"
|
||||
bl_description = "Stop tracking modification on the target datablock." + \
|
||||
"The datablock will no longer be updated for others client. "
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
property_path: bpy.props.StringProperty(default="None")
|
||||
@ -350,9 +381,8 @@ class SessionPropertyRemoveOperator(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client
|
||||
try:
|
||||
client.remove(self.property_path)
|
||||
session.remove(self.property_path)
|
||||
|
||||
return {"FINISHED"}
|
||||
except: # NonAuthorizedOperationError:
|
||||
@ -364,11 +394,12 @@ class SessionPropertyRemoveOperator(bpy.types.Operator):
|
||||
|
||||
class SessionPropertyRightOperator(bpy.types.Operator):
|
||||
bl_idname = "session.right"
|
||||
bl_label = "Change owner to"
|
||||
bl_description = "Change owner of specified datablock"
|
||||
bl_label = "Change modification rights"
|
||||
bl_description = "Modify the owner of the target datablock"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
key: bpy.props.StringProperty(default="None")
|
||||
recursive: bpy.props.BoolProperty(default=True)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@ -382,15 +413,20 @@ class SessionPropertyRightOperator(bpy.types.Operator):
|
||||
layout = self.layout
|
||||
runtime_settings = context.window_manager.session
|
||||
|
||||
col = layout.column()
|
||||
col.prop(runtime_settings, "clients")
|
||||
row = layout.row()
|
||||
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):
|
||||
runtime_settings = context.window_manager.session
|
||||
global client
|
||||
|
||||
if client:
|
||||
client.change_owner(self.key, runtime_settings.clients)
|
||||
if session:
|
||||
session.change_owner(self.key,
|
||||
runtime_settings.clients,
|
||||
recursive=self.recursive)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@ -436,11 +472,10 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
if event.type == 'TIMER':
|
||||
area, region, rv3d = presence.view3d_find()
|
||||
global client
|
||||
area, region, rv3d = view3d_find()
|
||||
|
||||
if client:
|
||||
target_ref = client.online_users.get(self.target_client)
|
||||
if session:
|
||||
target_ref = session.online_users.get(self.target_client)
|
||||
|
||||
if target_ref:
|
||||
target_scene = target_ref['metadata']['scene_current']
|
||||
@ -511,10 +546,8 @@ class SessionSnapTimeOperator(bpy.types.Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
if event.type == 'TIMER':
|
||||
global client
|
||||
|
||||
if client:
|
||||
target_ref = client.online_users.get(self.target_client)
|
||||
if session:
|
||||
target_ref = session.online_users.get(self.target_client)
|
||||
|
||||
if target_ref:
|
||||
context.scene.frame_current = target_ref['metadata']['frame_current']
|
||||
@ -526,28 +559,31 @@ class SessionSnapTimeOperator(bpy.types.Operator):
|
||||
|
||||
class SessionApply(bpy.types.Operator):
|
||||
bl_idname = "session.apply"
|
||||
bl_label = "apply selected block into blender"
|
||||
bl_description = "Apply selected block into blender"
|
||||
bl_label = "Revert"
|
||||
bl_description = "Revert the selected datablock from his cached" + \
|
||||
" version."
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
target: bpy.props.StringProperty()
|
||||
reset_dependencies: bpy.props.BoolProperty(default=False)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client
|
||||
|
||||
client.apply(self.target)
|
||||
logging.debug(f"Running apply on {self.target}")
|
||||
session.apply(self.target,
|
||||
force=True,
|
||||
force_dependencies=self.reset_dependencies)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SessionCommit(bpy.types.Operator):
|
||||
bl_idname = "session.commit"
|
||||
bl_label = "commit and push selected datablock to server"
|
||||
bl_description = "commit and push selected datablock to server"
|
||||
bl_label = "Force server update"
|
||||
bl_description = "Commit and push the target datablock to server"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
target: bpy.props.StringProperty()
|
||||
@ -557,10 +593,9 @@ class SessionCommit(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client
|
||||
# client.get(uuid=target).diff()
|
||||
client.commit(uuid=self.target)
|
||||
client.push(self.target)
|
||||
# session.get(uuid=target).diff()
|
||||
session.commit(uuid=self.target)
|
||||
session.push(self.target)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@ -578,16 +613,15 @@ class ApplyArmatureOperator(bpy.types.Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
if event.type == 'TIMER':
|
||||
global client
|
||||
if client and client.state['STATE'] == STATE_ACTIVE:
|
||||
nodes = client.list(filter=bl_types.bl_armature.BlArmature)
|
||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||
nodes = session.list(filter=bl_types.bl_armature.BlArmature)
|
||||
|
||||
for node in nodes:
|
||||
node_ref = client.get(uuid=node)
|
||||
node_ref = session.get(uuid=node)
|
||||
|
||||
if node_ref.state == FETCHED:
|
||||
try:
|
||||
client.apply(node)
|
||||
session.apply(node)
|
||||
except Exception as e:
|
||||
logging.error("Fail to apply armature: {e}")
|
||||
|
||||
@ -608,6 +642,35 @@ class ApplyArmatureOperator(bpy.types.Operator):
|
||||
stop_modal_executor = False
|
||||
|
||||
|
||||
class ClearCache(bpy.types.Operator):
|
||||
"Clear local session cache"
|
||||
bl_idname = "session.clear_cache"
|
||||
bl_label = "Modal Executor Operator"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
cache_dir = utils.get_preferences().cache_directory
|
||||
try:
|
||||
for root, dirs, files in os.walk(cache_dir):
|
||||
for name in files:
|
||||
Path(root, name).unlink()
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout
|
||||
row.label(text=f" Do you really want to remove local cache ? ")
|
||||
|
||||
|
||||
classes = (
|
||||
SessionStartOperator,
|
||||
SessionStopOperator,
|
||||
@ -620,7 +683,7 @@ classes = (
|
||||
ApplyArmatureOperator,
|
||||
SessionKickOperator,
|
||||
SessionInitOperator,
|
||||
|
||||
ClearCache,
|
||||
)
|
||||
|
||||
|
||||
@ -632,32 +695,28 @@ def sanitize_deps_graph(dummy):
|
||||
A future solution should be to avoid storing dataclock reference...
|
||||
|
||||
"""
|
||||
global client
|
||||
|
||||
if client and client.state['STATE'] == STATE_ACTIVE:
|
||||
for node_key in client.list():
|
||||
client.get(node_key).resolve()
|
||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||
for node_key in session.list():
|
||||
session.get(node_key).resolve()
|
||||
|
||||
|
||||
@persistent
|
||||
def load_pre_handler(dummy):
|
||||
global client
|
||||
|
||||
if client and client.state['STATE'] in [STATE_ACTIVE, STATE_SYNCING]:
|
||||
if session and session.state['STATE'] in [STATE_ACTIVE, STATE_SYNCING]:
|
||||
bpy.ops.session.stop()
|
||||
|
||||
|
||||
@persistent
|
||||
def update_client_frame(scene):
|
||||
if client and client.state['STATE'] == STATE_ACTIVE:
|
||||
client.update_user_metadata({
|
||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||
session.update_user_metadata({
|
||||
'frame_current': scene.frame_current
|
||||
})
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_evaluation(scene):
|
||||
if client and client.state['STATE'] == STATE_ACTIVE:
|
||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||
context = bpy.context
|
||||
blender_depsgraph = bpy.context.view_layer.depsgraph
|
||||
dependency_updates = [u for u in blender_depsgraph.updates]
|
||||
@ -669,19 +728,19 @@ def depsgraph_evaluation(scene):
|
||||
# Is the object tracked ?
|
||||
if update.id.uuid:
|
||||
# Retrieve local version
|
||||
node = client.get(update.id.uuid)
|
||||
node = session.get(update.id.uuid)
|
||||
|
||||
# Check our right on this update:
|
||||
# - if its ours or ( under common and diff), launch the
|
||||
# update process
|
||||
# - if its to someone else, ignore the update (go deeper ?)
|
||||
if node and node.owner in [client.id, RP_COMMON] and node.state == UP:
|
||||
if node and node.owner in [session.id, RP_COMMON] and node.state == UP:
|
||||
# Avoid slow geometry update
|
||||
if 'EDIT' in context.mode and \
|
||||
not settings.enable_editmode_updates:
|
||||
not settings.sync_during_editmode:
|
||||
break
|
||||
|
||||
client.stash(node.uuid)
|
||||
session.stash(node.uuid)
|
||||
else:
|
||||
# Distant update
|
||||
continue
|
||||
@ -692,6 +751,7 @@ def depsgraph_evaluation(scene):
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
|
||||
@ -703,11 +763,8 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
global client
|
||||
|
||||
if client and client.state['STATE'] == 2:
|
||||
client.disconnect()
|
||||
client = None
|
||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||
session.disconnect()
|
||||
|
||||
from bpy.utils import unregister_class
|
||||
for cls in reversed(classes):
|
||||
|
@ -20,10 +20,14 @@ import logging
|
||||
import bpy
|
||||
import string
|
||||
import re
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from . import bl_types, environment, addon_updater_ops, presence, ui
|
||||
from .utils import get_preferences, get_expanded_icon
|
||||
from replication.constants import RP_COMMON
|
||||
from replication.interface import session
|
||||
|
||||
IP_EXPR = re.compile('\d+\.\d+\.\d+\.\d+')
|
||||
|
||||
@ -37,7 +41,7 @@ def randomColor():
|
||||
|
||||
|
||||
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
|
||||
return ''.join(random.choices(lettersAndDigits, k=stringLength))
|
||||
|
||||
@ -64,10 +68,20 @@ def update_port(self, context):
|
||||
if self.ipc_port < max_port and \
|
||||
self['ipc_port'] >= self.port:
|
||||
logging.error(
|
||||
"IPC Port in conflic with the port, assigning a random value")
|
||||
"IPC Port in conflict with the port, assigning a random value")
|
||||
self['ipc_port'] = random.randrange(self.port+4, 10000)
|
||||
|
||||
|
||||
def update_directory(self, context):
|
||||
new_dir = Path(self.cache_directory)
|
||||
if new_dir.exists() and any(Path(self.cache_directory).iterdir()):
|
||||
logging.error("The folder is not empty, choose another one.")
|
||||
self['cache_directory'] = environment.DEFAULT_CACHE_DIR
|
||||
elif not new_dir.exists():
|
||||
logging.info("Target cache folder doesn't exist, creating it.")
|
||||
os.makedirs(self.cache_directory, exist_ok=True)
|
||||
|
||||
|
||||
def set_log_level(self, value):
|
||||
logging.getLogger().setLevel(value)
|
||||
|
||||
@ -86,11 +100,49 @@ class ReplicatedDatablock(bpy.types.PropertyGroup):
|
||||
icon: bpy.props.StringProperty()
|
||||
|
||||
|
||||
def set_sync_render_settings(self, value):
|
||||
self['sync_render_settings'] = value
|
||||
if session and bpy.context.scene.uuid and value:
|
||||
bpy.ops.session.apply('INVOKE_DEFAULT',
|
||||
target=bpy.context.scene.uuid,
|
||||
reset_dependencies=False)
|
||||
|
||||
|
||||
def set_sync_active_camera(self, value):
|
||||
self['sync_active_camera'] = value
|
||||
|
||||
if session and bpy.context.scene.uuid and value:
|
||||
bpy.ops.session.apply('INVOKE_DEFAULT',
|
||||
target=bpy.context.scene.uuid,
|
||||
reset_dependencies=False)
|
||||
|
||||
|
||||
class ReplicationFlags(bpy.types.PropertyGroup):
|
||||
def get_sync_render_settings(self):
|
||||
return self.get('sync_render_settings', True)
|
||||
|
||||
def get_sync_active_camera(self):
|
||||
return self.get('sync_active_camera', True)
|
||||
|
||||
sync_render_settings: bpy.props.BoolProperty(
|
||||
name="Synchronize render settings",
|
||||
description="Synchronize render settings (eevee and cycles only)",
|
||||
default=True)
|
||||
default=False,
|
||||
set=set_sync_render_settings,
|
||||
get=get_sync_render_settings
|
||||
)
|
||||
sync_during_editmode: bpy.props.BoolProperty(
|
||||
name="Edit mode updates",
|
||||
description="Enable objects update in edit mode (! Impact performances !)",
|
||||
default=False
|
||||
)
|
||||
sync_active_camera: bpy.props.BoolProperty(
|
||||
name="Synchronize active camera",
|
||||
description="Synchronize the active camera",
|
||||
default=True,
|
||||
get=get_sync_active_camera,
|
||||
set=set_sync_active_camera
|
||||
)
|
||||
|
||||
|
||||
class SessionPrefs(bpy.types.AddonPreferences):
|
||||
@ -122,9 +174,9 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
)
|
||||
ipc_port: bpy.props.IntProperty(
|
||||
name="ipc_port",
|
||||
description='internal ttl port(only usefull for multiple local instances)',
|
||||
default=5561,
|
||||
update=update_port
|
||||
description='internal ttl port(only useful for multiple local instances)',
|
||||
default=random.randrange(5570, 70000),
|
||||
update=update_port,
|
||||
)
|
||||
init_method: bpy.props.EnumProperty(
|
||||
name='init_method',
|
||||
@ -136,7 +188,8 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
cache_directory: bpy.props.StringProperty(
|
||||
name="cache directory",
|
||||
subtype="DIR_PATH",
|
||||
default=environment.DEFAULT_CACHE_DIR)
|
||||
default=environment.DEFAULT_CACHE_DIR,
|
||||
update=update_directory)
|
||||
connection_timeout: bpy.props.IntProperty(
|
||||
name='connection timeout',
|
||||
description='connection timeout before disconnection',
|
||||
@ -157,9 +210,9 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
description='Dependency graph uppdate rate (milliseconds)',
|
||||
default=100
|
||||
)
|
||||
enable_editmode_updates: bpy.props.BoolProperty(
|
||||
name="Edit mode updates",
|
||||
description="Enable objects update in edit mode (! Impact performances !)",
|
||||
clear_memory_filecache: bpy.props.BoolProperty(
|
||||
name="Clear memory filecache",
|
||||
description="Remove filecache from memory",
|
||||
default=False
|
||||
)
|
||||
# for UI
|
||||
@ -167,7 +220,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
name="Category",
|
||||
description="Preferences Category",
|
||||
items=[
|
||||
('CONFIG', "Configuration", "Configuration about this add-on"),
|
||||
('CONFIG', "Configuration", "Configuration of this add-on"),
|
||||
('UPDATE', "Update", "Update this add-on"),
|
||||
],
|
||||
default='CONFIG'
|
||||
@ -230,6 +283,12 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
description="sidebar_advanced_net_expanded",
|
||||
default=False
|
||||
)
|
||||
sidebar_advanced_cache_expanded: bpy.props.BoolProperty(
|
||||
name="sidebar_advanced_cache_expanded",
|
||||
description="sidebar_advanced_cache_expanded",
|
||||
default=False
|
||||
)
|
||||
|
||||
auto_check_update: bpy.props.BoolProperty(
|
||||
name="Auto-check for Update",
|
||||
description="If enabled, auto-check for updates using an interval",
|
||||
@ -280,7 +339,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
# USER INFORMATIONS
|
||||
box = grid.box()
|
||||
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),
|
||||
emboss=False)
|
||||
if self.conf_session_identity_expanded:
|
||||
@ -290,7 +349,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
# NETWORK SETTINGS
|
||||
box = grid.box()
|
||||
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),
|
||||
emboss=False)
|
||||
|
||||
@ -343,6 +402,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
emboss=False)
|
||||
if self.conf_session_cache_expanded:
|
||||
box.row().prop(self, "cache_directory", text="Cache directory")
|
||||
box.row().prop(self, "clear_memory_filecache", text="Clear memory filecache")
|
||||
|
||||
# INTERFACE SETTINGS
|
||||
box = grid.box()
|
||||
@ -383,9 +443,9 @@ def client_list_callback(scene, context):
|
||||
items = [(RP_COMMON, RP_COMMON, "")]
|
||||
|
||||
username = get_preferences().username
|
||||
cli = operators.client
|
||||
if cli:
|
||||
client_ids = cli.online_users.keys()
|
||||
|
||||
if session:
|
||||
client_ids = session.online_users.keys()
|
||||
for id in client_ids:
|
||||
name_desc = id
|
||||
if id == username:
|
||||
@ -421,25 +481,26 @@ class SessionProps(bpy.types.PropertyGroup):
|
||||
name="Presence overlay",
|
||||
description='Enable overlay drawing module',
|
||||
default=True,
|
||||
update=presence.update_presence
|
||||
)
|
||||
presence_show_selected: bpy.props.BoolProperty(
|
||||
name="Show selected objects",
|
||||
description='Enable selection overlay ',
|
||||
default=True,
|
||||
update=presence.update_overlay_settings
|
||||
)
|
||||
presence_show_user: bpy.props.BoolProperty(
|
||||
name="Show users",
|
||||
description='Enable user overlay ',
|
||||
default=True,
|
||||
update=presence.update_overlay_settings
|
||||
)
|
||||
presence_show_far_user: bpy.props.BoolProperty(
|
||||
name="Show users on different scenes",
|
||||
description="Show user on different scenes",
|
||||
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(
|
||||
name="filter_owned",
|
||||
|
@ -19,6 +19,7 @@
|
||||
import copy
|
||||
import logging
|
||||
import math
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import bgl
|
||||
@ -28,13 +29,17 @@ import gpu
|
||||
import mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
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
|
||||
|
||||
renderer = None
|
||||
# Helper functions
|
||||
|
||||
|
||||
def view3d_find():
|
||||
def view3d_find() -> tuple:
|
||||
""" Find the first 'VIEW_3D' windows found in areas
|
||||
|
||||
:return: tuple(Area, Region, RegionView3D)
|
||||
@ -56,36 +61,48 @@ def refresh_3d_view():
|
||||
if area and region and rv3d:
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def refresh_sidebar_view():
|
||||
""" Refresh the blender sidebar
|
||||
""" Refresh the blender viewport sidebar
|
||||
"""
|
||||
area, region, rv3d = view3d_find()
|
||||
|
||||
if area:
|
||||
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]
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
if coords and region and rv3d:
|
||||
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coords)
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coords)
|
||||
target = ray_origin + view_vector * distance
|
||||
|
||||
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 = [
|
||||
(-radius, -radius, -radius), (+radius, -radius, -radius),
|
||||
(-radius, +radius, -radius), (+radius, +radius, -radius),
|
||||
@ -93,264 +110,380 @@ def get_default_bbox(obj, radius):
|
||||
(-radius, +radius, +radius), (+radius, +radius, +radius)]
|
||||
|
||||
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)
|
||||
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()
|
||||
|
||||
v1 = [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]
|
||||
v1 = v2 = v3 = v4 = v5 = v6 = v7 = [0, 0, 0]
|
||||
|
||||
if area and region and rv3d:
|
||||
width = region.width
|
||||
height = region.height
|
||||
|
||||
v1 = get_target(region, rv3d, (0, 0))
|
||||
v3 = get_target(region, rv3d, (0, height))
|
||||
v2 = get_target(region, rv3d, (width, height))
|
||||
v4 = get_target(region, rv3d, (width, 0))
|
||||
v1 = project_to_viewport(region, rv3d, (0, 0))
|
||||
v3 = project_to_viewport(region, rv3d, (0, height))
|
||||
v2 = project_to_viewport(region, rv3d, (width, height))
|
||||
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)
|
||||
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]
|
||||
|
||||
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()
|
||||
if area and region and rv3d:
|
||||
return view3d_utils.location_3d_to_region_2d(region, rv3d, coords)
|
||||
else:
|
||||
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(
|
||||
corner) for corner in object.bound_box]
|
||||
corner) for corner in object.bound_box]
|
||||
|
||||
|
||||
return [(point.x, point.y, point.z)
|
||||
for point in bbox_corners]
|
||||
return [(point.x, point.y, point.z) 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()
|
||||
|
||||
if area and region and rv3d:
|
||||
if area and region and rv3d:
|
||||
return [list(v) for v in rv3d.view_matrix]
|
||||
|
||||
def update_presence(self, context):
|
||||
global renderer
|
||||
|
||||
if 'renderer' in globals() and hasattr(renderer, 'run'):
|
||||
if self.enable_presence:
|
||||
renderer.run()
|
||||
class Widget(object):
|
||||
""" Base class to define an interface element
|
||||
"""
|
||||
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:
|
||||
renderer.stop()
|
||||
return None
|
||||
|
||||
def poll(self):
|
||||
if self.data is None:
|
||||
return False
|
||||
|
||||
def update_overlay_settings(self, context):
|
||||
global renderer
|
||||
scene_current = self.data.get('scene_current')
|
||||
view_corners = self.data.get('view_corners')
|
||||
|
||||
if renderer and not self.presence_show_selected:
|
||||
renderer.flush_selection()
|
||||
if renderer and not self.presence_show_user:
|
||||
renderer.flush_users()
|
||||
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
|
||||
|
||||
|
||||
class DrawFactory(object):
|
||||
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):
|
||||
def draw(self):
|
||||
location = self.data.get('view_corners')
|
||||
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(
|
||||
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.glEnable(bgl.GL_DEPTH_TEST)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
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
|
||||
|
||||
position = None
|
||||
|
||||
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' and hasattr(obj, 'bound_box'):
|
||||
positions = get_bb_coords_from_obj(obj, instance=ob)
|
||||
break
|
||||
elif hasattr(ob, 'bound_box'):
|
||||
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))
|
||||
positions = get_bb_coords_from_obj(ob)
|
||||
if positions is None:
|
||||
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))
|
||||
|
||||
positions = bbox_from_obj(ob, ob.scale.x)
|
||||
|
||||
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
||||
batch = batch_for_shader(
|
||||
shader,
|
||||
'LINES',
|
||||
{"pos": positions},
|
||||
indices=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'
|
||||
|
||||
@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):
|
||||
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]
|
||||
|
||||
blf.position(0, 10, 20, 0)
|
||||
blf.size(0, 16, 45)
|
||||
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:
|
||||
for shader, batch, color in self.d3d_items.values():
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
except Exception:
|
||||
logging.error("3D Exception")
|
||||
for widget in self.widgets.values():
|
||||
if widget.draw_type == 'POST_VIEW' and widget.poll():
|
||||
widget.draw()
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Post view widget exception: {e} \n {traceback.print_exc()}")
|
||||
|
||||
def draw2d_callback(self):
|
||||
for position, font, color in self.d2d_items.values():
|
||||
try:
|
||||
coords = get_client_2d(position)
|
||||
def post_pixel_callback(self):
|
||||
try:
|
||||
for widget in self.widgets.values():
|
||||
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:
|
||||
logging.error("2D EXCEPTION")
|
||||
this = sys.modules[__name__]
|
||||
this.renderer = DrawFactory()
|
||||
|
||||
|
||||
def register():
|
||||
global renderer
|
||||
renderer = DrawFactory()
|
||||
this.renderer.register_handlers()
|
||||
|
||||
this.renderer.add_widget("session_status", SessionStatusWidget())
|
||||
|
||||
|
||||
def unregister():
|
||||
global renderer
|
||||
renderer.unregister_handlers()
|
||||
this.renderer.unregister_handlers()
|
||||
|
||||
del renderer
|
||||
this.renderer.clear_widgets()
|
||||
|
262
multi_user/ui.py
@ -18,8 +18,7 @@
|
||||
|
||||
import bpy
|
||||
|
||||
from . import operators
|
||||
from .utils import get_preferences, get_expanded_icon
|
||||
from .utils import get_preferences, get_expanded_icon, get_folder_size, get_state_str
|
||||
from replication.constants import (ADDED, ERROR, FETCHED,
|
||||
MODIFIED, RP_COMMON, UP,
|
||||
STATE_ACTIVE, STATE_AUTH,
|
||||
@ -29,13 +28,15 @@ from replication.constants import (ADDED, ERROR, FETCHED,
|
||||
STATE_LOBBY,
|
||||
STATE_LAUNCHING_SERVICES)
|
||||
from replication import __version__
|
||||
from replication.interface import session
|
||||
|
||||
ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
|
||||
'TRIA_UP', # COMMITED
|
||||
'KEYTYPE_KEYFRAME_VEC', # PUSHED
|
||||
'TRIA_DOWN', # FETCHED
|
||||
'FILE_REFRESH', # UP
|
||||
'TRIA_UP'] # CHANGED
|
||||
'RECOVER_LAST', # RESET
|
||||
'TRIA_UP', # CHANGED
|
||||
'ERROR'] # ERROR
|
||||
|
||||
|
||||
def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█', fill_empty=' '):
|
||||
@ -59,32 +60,6 @@ def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=
|
||||
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):
|
||||
"""Settings panel"""
|
||||
bl_idname = "MULTIUSER_SETTINGS_PT_panel"
|
||||
@ -95,9 +70,9 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
if operators.client and operators.client.state['STATE'] != STATE_INITIAL:
|
||||
cli_state = operators.client.state
|
||||
state = operators.client.state.get('STATE')
|
||||
if session and session.state['STATE'] != STATE_INITIAL:
|
||||
cli_state = session.state
|
||||
state = session.state.get('STATE')
|
||||
connection_icon = "KEYTYPE_MOVING_HOLD_VEC"
|
||||
|
||||
if state == STATE_ACTIVE:
|
||||
@ -118,65 +93,43 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
|
||||
if hasattr(context.window_manager, 'session'):
|
||||
# STATE INITIAL
|
||||
if not operators.client \
|
||||
or (operators.client and operators.client.state['STATE'] == STATE_INITIAL):
|
||||
if not session \
|
||||
or (session and session.state['STATE'] == STATE_INITIAL):
|
||||
pass
|
||||
else:
|
||||
cli_state = operators.client.state
|
||||
|
||||
|
||||
cli_state = session.state
|
||||
row = layout.row()
|
||||
|
||||
current_state = cli_state['STATE']
|
||||
info_msg = None
|
||||
|
||||
# STATE ACTIVE
|
||||
if current_state in [STATE_ACTIVE]:
|
||||
row.operator("session.stop", icon='QUIT', text="Exit")
|
||||
row = layout.row()
|
||||
if runtime_settings.is_host:
|
||||
row = row.box()
|
||||
row.label(text=f"LAN: {runtime_settings.internet_ip}", icon='INFO')
|
||||
row = layout.row()
|
||||
row = row.split(factor=0.3)
|
||||
row.prop(settings.sync_flags, "sync_render_settings",text="",icon_only=True, icon='SCENE')
|
||||
row.prop(settings.sync_flags, "sync_during_editmode", text="",icon_only=True, icon='EDITMODE_HLT')
|
||||
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='OBJECT_DATAMODE')
|
||||
|
||||
row= layout.row()
|
||||
|
||||
if current_state in [STATE_ACTIVE] and runtime_settings.is_host:
|
||||
info_msg = f"LAN: {runtime_settings.internet_ip}"
|
||||
if current_state == STATE_LOBBY:
|
||||
row = row.box()
|
||||
row.label(text=f"Waiting the session to start", icon='INFO')
|
||||
row = layout.row()
|
||||
row.operator("session.stop", icon='QUIT', text="Exit")
|
||||
# CONNECTION STATE
|
||||
elif current_state in [STATE_SRV_SYNC,
|
||||
STATE_SYNCING,
|
||||
STATE_AUTH,
|
||||
STATE_CONFIG,
|
||||
STATE_WAITING]:
|
||||
info_msg = "Waiting for the session to start."
|
||||
|
||||
if cli_state['STATE'] in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]:
|
||||
box = row.box()
|
||||
box.label(text=printProgressBar(
|
||||
cli_state['CURRENT'],
|
||||
cli_state['TOTAL'],
|
||||
length=16
|
||||
))
|
||||
if info_msg:
|
||||
info_box = row.box()
|
||||
info_box.row().label(text=info_msg,icon='INFO')
|
||||
|
||||
row = layout.row()
|
||||
row.operator("session.stop", icon='QUIT', text="CANCEL")
|
||||
elif current_state == STATE_QUITTING:
|
||||
row = layout.row()
|
||||
box = row.box()
|
||||
|
||||
num_online_services = 0
|
||||
for name, state in operators.client.services_state.items():
|
||||
if state == STATE_ACTIVE:
|
||||
num_online_services += 1
|
||||
|
||||
total_online_services = len(
|
||||
operators.client.services_state)
|
||||
|
||||
box.label(text=printProgressBar(
|
||||
total_online_services-num_online_services,
|
||||
total_online_services,
|
||||
# Progress bar
|
||||
if current_state in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]:
|
||||
info_box = row.box()
|
||||
info_box.row().label(text=printProgressBar(
|
||||
cli_state['CURRENT'],
|
||||
cli_state['TOTAL'],
|
||||
length=16
|
||||
))
|
||||
|
||||
layout.row().operator("session.stop", icon='QUIT', text="Exit")
|
||||
|
||||
class SESSION_PT_settings_network(bpy.types.Panel):
|
||||
bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel"
|
||||
@ -187,8 +140,8 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return not operators.client \
|
||||
or (operators.client and operators.client.state['STATE'] == 0)
|
||||
return not session \
|
||||
or (session and session.state['STATE'] == 0)
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='URL')
|
||||
@ -245,8 +198,8 @@ class SESSION_PT_settings_user(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return not operators.client \
|
||||
or (operators.client and operators.client.state['STATE'] == 0)
|
||||
return not session \
|
||||
or (session and session.state['STATE'] == 0)
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='USER')
|
||||
@ -276,8 +229,8 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return not operators.client \
|
||||
or (operators.client and operators.client.state['STATE'] == 0)
|
||||
return not session \
|
||||
or (session and session.state['STATE'] == 0)
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='PREFERENCES')
|
||||
@ -320,10 +273,12 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
||||
replication_section_row = replication_section.row()
|
||||
replication_section_row.prop(settings.sync_flags, "sync_render_settings")
|
||||
replication_section_row = replication_section.row()
|
||||
|
||||
replication_section_row.prop(settings, "enable_editmode_updates")
|
||||
replication_section_row.prop(settings.sync_flags, "sync_active_camera")
|
||||
replication_section_row = replication_section.row()
|
||||
if settings.enable_editmode_updates:
|
||||
|
||||
replication_section_row.prop(settings.sync_flags, "sync_during_editmode")
|
||||
replication_section_row = replication_section.row()
|
||||
if settings.sync_flags.sync_during_editmode:
|
||||
warning = replication_section_row.box()
|
||||
warning.label(text="Don't use this with heavy meshes !", icon='ERROR')
|
||||
replication_section_row = replication_section.row()
|
||||
@ -356,6 +311,23 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
||||
replication_timers.label(text="Update rate (ms):")
|
||||
replication_timers.prop(settings, "depsgraph_update_rate", text="")
|
||||
|
||||
cache_section = layout.row().box()
|
||||
cache_section.prop(
|
||||
settings,
|
||||
"sidebar_advanced_cache_expanded",
|
||||
text="Cache",
|
||||
icon=get_expanded_icon(settings.sidebar_advanced_cache_expanded),
|
||||
emboss=False)
|
||||
if settings.sidebar_advanced_cache_expanded:
|
||||
cache_section_row = cache_section.row()
|
||||
cache_section_row.label(text="Cache directory:")
|
||||
cache_section_row = cache_section.row()
|
||||
cache_section_row.prop(settings, "cache_directory", text="")
|
||||
cache_section_row = cache_section.row()
|
||||
cache_section_row.label(text="Clear memory filecache:")
|
||||
cache_section_row.prop(settings, "clear_memory_filecache", text="")
|
||||
cache_section_row = cache_section.row()
|
||||
cache_section_row.operator('session.clear_cache', text=f"Clear cache ({get_folder_size(settings.cache_directory)})")
|
||||
log_section = layout.row().box()
|
||||
log_section.prop(
|
||||
settings,
|
||||
@ -377,7 +349,7 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return operators.client and operators.client.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]
|
||||
return session and session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='USER')
|
||||
@ -408,7 +380,7 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
if active_user != 0 and active_user.username != settings.username:
|
||||
row = layout.row()
|
||||
user_operations = row.split()
|
||||
if operators.client.state['STATE'] == STATE_ACTIVE:
|
||||
if session.state['STATE'] == STATE_ACTIVE:
|
||||
|
||||
user_operations.alert = context.window_manager.session.time_snap_running
|
||||
user_operations.operator(
|
||||
@ -422,7 +394,7 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
text="",
|
||||
icon='TIME').target_client = active_user.username
|
||||
|
||||
if operators.client.online_users[settings.username]['admin']:
|
||||
if session.online_users[settings.username]['admin']:
|
||||
user_operations.operator(
|
||||
"session.kick",
|
||||
text="",
|
||||
@ -431,7 +403,6 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
|
||||
class SESSION_UL_users(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
|
||||
session = operators.client
|
||||
settings = get_preferences()
|
||||
is_local_user = item.username == settings.username
|
||||
ping = '-'
|
||||
@ -466,8 +437,8 @@ class SESSION_PT_presence(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return not operators.client \
|
||||
or (operators.client and operators.client.state['STATE'] in [STATE_INITIAL, STATE_ACTIVE])
|
||||
return not session \
|
||||
or (session and session.state['STATE'] in [STATE_INITIAL, STATE_ACTIVE])
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.prop(context.window_manager.session,
|
||||
@ -479,54 +450,25 @@ class SESSION_PT_presence(bpy.types.Panel):
|
||||
settings = context.window_manager.session
|
||||
layout.active = settings.enable_presence
|
||||
col = layout.column()
|
||||
col.prop(settings, "presence_show_session_status")
|
||||
col.prop(settings, "presence_show_selected")
|
||||
col.prop(settings, "presence_show_user")
|
||||
row = layout.column()
|
||||
row.active = settings.presence_show_user
|
||||
row.prop(settings, "presence_show_far_user")
|
||||
|
||||
|
||||
class SESSION_PT_services(bpy.types.Panel):
|
||||
bl_idname = "MULTIUSER_SERVICE_PT_panel"
|
||||
bl_label = "Services"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return operators.client and operators.client.state['STATE'] == 2
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='FILE_CACHE')
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
online_users = context.window_manager.online_users
|
||||
selected_user = context.window_manager.user_index
|
||||
settings = context.window_manager.session
|
||||
active_user = online_users[selected_user] if len(online_users)-1 >= selected_user else 0
|
||||
|
||||
# Create a simple row.
|
||||
for name, state in operators.client.services_state.items():
|
||||
row = layout.row()
|
||||
row.label(text=name)
|
||||
row.label(text=get_state_str(state))
|
||||
|
||||
|
||||
def draw_property(context, parent, property_uuid, level=0):
|
||||
settings = get_preferences()
|
||||
runtime_settings = context.window_manager.session
|
||||
item = operators.client.get(uuid=property_uuid)
|
||||
|
||||
if item.state == ERROR:
|
||||
return
|
||||
item = session.get(uuid=property_uuid)
|
||||
|
||||
area_msg = parent.row(align=True)
|
||||
if level > 0:
|
||||
for i in range(level):
|
||||
area_msg.label(text="")
|
||||
|
||||
if item.state == ERROR:
|
||||
area_msg.alert=True
|
||||
else:
|
||||
area_msg.alert=False
|
||||
|
||||
line = area_msg.box()
|
||||
|
||||
name = item.data['name'] if item.data else item.uuid
|
||||
@ -539,8 +481,8 @@ def draw_property(context, parent, property_uuid, level=0):
|
||||
|
||||
# Operations
|
||||
|
||||
have_right_to_modify = item.owner == settings.username or \
|
||||
item.owner == RP_COMMON
|
||||
have_right_to_modify = (item.owner == settings.username or \
|
||||
item.owner == RP_COMMON) and item.state != ERROR
|
||||
|
||||
if have_right_to_modify:
|
||||
detail_item_box.operator(
|
||||
@ -550,10 +492,12 @@ def draw_property(context, parent, property_uuid, level=0):
|
||||
detail_item_box.separator()
|
||||
|
||||
if item.state in [FETCHED, UP]:
|
||||
detail_item_box.operator(
|
||||
apply = detail_item_box.operator(
|
||||
"session.apply",
|
||||
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]:
|
||||
detail_item_box.operator(
|
||||
"session.commit",
|
||||
@ -576,7 +520,6 @@ def draw_property(context, parent, property_uuid, level=0):
|
||||
else:
|
||||
detail_item_box.label(text="", icon="DECORATE_LOCKED")
|
||||
|
||||
|
||||
class SESSION_PT_repository(bpy.types.Panel):
|
||||
bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
|
||||
bl_label = "Repository"
|
||||
@ -586,7 +529,6 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
session = operators.client
|
||||
settings = get_preferences()
|
||||
admin = False
|
||||
|
||||
@ -595,9 +537,9 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
if usr:
|
||||
admin = usr['admin']
|
||||
return hasattr(context.window_manager, 'session') and \
|
||||
operators.client and \
|
||||
(operators.client.state['STATE'] == STATE_ACTIVE or \
|
||||
operators.client.state['STATE'] == STATE_LOBBY and admin)
|
||||
session and \
|
||||
(session.state['STATE'] == STATE_ACTIVE or \
|
||||
session.state['STATE'] == STATE_LOBBY and admin)
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
|
||||
@ -609,7 +551,6 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
settings = get_preferences()
|
||||
runtime_settings = context.window_manager.session
|
||||
|
||||
session = operators.client
|
||||
usr = session.online_users.get(settings.username)
|
||||
|
||||
row = layout.row()
|
||||
@ -635,11 +576,11 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
types_filter = [t.type_name for t in settings.supported_datablocks
|
||||
if t.use_as_filter]
|
||||
|
||||
key_to_filter = operators.client.list(
|
||||
filter_owner=settings.username) if runtime_settings.filter_owned else operators.client.list()
|
||||
key_to_filter = session.list(
|
||||
filter_owner=settings.username) if runtime_settings.filter_owned else session.list()
|
||||
|
||||
client_keys = [key for key in key_to_filter
|
||||
if operators.client.get(uuid=key).str_type
|
||||
if session.get(uuid=key).str_type
|
||||
in types_filter]
|
||||
|
||||
if client_keys:
|
||||
@ -655,6 +596,36 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
else:
|
||||
row.label(text="Waiting to start")
|
||||
|
||||
class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'HEADER'
|
||||
bl_parent_id = 'VIEW3D_PT_overlay'
|
||||
bl_label = "Multi-user"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
view = context.space_data
|
||||
overlay = view.overlay
|
||||
display_all = overlay.show_overlays
|
||||
|
||||
col = layout.column()
|
||||
|
||||
row = col.row(align=True)
|
||||
settings = context.window_manager.session
|
||||
layout.active = settings.enable_presence
|
||||
col = layout.column()
|
||||
col.prop(settings, "presence_show_session_status")
|
||||
col.prop(settings, "presence_show_selected")
|
||||
col.prop(settings, "presence_show_user")
|
||||
|
||||
row = layout.column()
|
||||
row.active = settings.presence_show_user
|
||||
row.prop(settings, "presence_show_far_user")
|
||||
|
||||
classes = (
|
||||
SESSION_UL_users,
|
||||
@ -664,9 +635,8 @@ classes = (
|
||||
SESSION_PT_presence,
|
||||
SESSION_PT_advanced_settings,
|
||||
SESSION_PT_user,
|
||||
SESSION_PT_services,
|
||||
SESSION_PT_repository,
|
||||
|
||||
VIEW3D_PT_overlay_session,
|
||||
)
|
||||
|
||||
|
||||
|
@ -21,13 +21,22 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from uuid import uuid4
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
import math
|
||||
|
||||
import bpy
|
||||
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):
|
||||
@ -47,7 +56,7 @@ def get_datablock_users(datablock):
|
||||
if hasattr(datablock, 'users_group') and datablock.users_scene:
|
||||
users.extend(list(datablock.users_scene))
|
||||
for datatype in supported_types:
|
||||
if datatype.bl_name != 'users':
|
||||
if datatype.bl_name != 'users' and hasattr(bpy.data, datatype.bl_name):
|
||||
root = getattr(bpy.data, datatype.bl_name)
|
||||
for item in root:
|
||||
if hasattr(item, 'data') and datablock == item.data or \
|
||||
@ -56,6 +65,32 @@ def get_datablock_users(datablock):
|
||||
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():
|
||||
for type_name in dir(bpy.data):
|
||||
try:
|
||||
@ -78,17 +113,6 @@ def resolve_from_id(id, optionnal_type=None):
|
||||
return root[id]
|
||||
return None
|
||||
|
||||
def get_datablock_from_uuid(uuid, default, ignore=[]):
|
||||
if not uuid:
|
||||
return default
|
||||
|
||||
for category in dir(bpy.data):
|
||||
root = getattr(bpy.data, category)
|
||||
if isinstance(root, Iterable) and category not in ignore:
|
||||
for item in root:
|
||||
if getattr(item, 'uuid', None) == uuid:
|
||||
return item
|
||||
return default
|
||||
|
||||
def get_preferences():
|
||||
return bpy.context.preferences.addons[__package__].preferences
|
||||
@ -103,3 +127,61 @@ def get_expanded_icon(prop: bpy.types.BoolProperty) -> str:
|
||||
return 'DISCLOSURE_TRI_DOWN'
|
||||
else:
|
||||
return 'DISCLOSURE_TRI_RIGHT'
|
||||
|
||||
|
||||
# Taken from here: https://stackoverflow.com/a/55659577
|
||||
def get_folder_size(folder):
|
||||
return ByteSize(sum(file.stat().st_size for file in Path(folder).rglob('*')))
|
||||
|
||||
|
||||
class ByteSize(int):
|
||||
|
||||
_kB = 1024
|
||||
_suffixes = 'B', 'kB', 'MB', 'GB', 'PB'
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
return super().__new__(cls, *args, **kwargs)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.bytes = self.B = int(self)
|
||||
self.kilobytes = self.kB = self / self._kB**1
|
||||
self.megabytes = self.MB = self / self._kB**2
|
||||
self.gigabytes = self.GB = self / self._kB**3
|
||||
self.petabytes = self.PB = self / self._kB**4
|
||||
*suffixes, last = self._suffixes
|
||||
suffix = next((
|
||||
suffix
|
||||
for suffix in suffixes
|
||||
if 1 < getattr(self, suffix) < self._kB
|
||||
), last)
|
||||
self.readable = suffix, getattr(self, suffix)
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __str__(self):
|
||||
return self.__format__('.2f')
|
||||
|
||||
def __repr__(self):
|
||||
return '{}({})'.format(self.__class__.__name__, super().__repr__())
|
||||
|
||||
def __format__(self, format_spec):
|
||||
suffix, val = self.readable
|
||||
return '{val:{fmt}} {suf}'.format(val=math.ceil(val), fmt=format_spec, suf=suffix)
|
||||
|
||||
def __sub__(self, other):
|
||||
return self.__class__(super().__sub__(other))
|
||||
|
||||
def __add__(self, other):
|
||||
return self.__class__(super().__add__(other))
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.__class__(super().__mul__(other))
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.__class__(super().__sub__(other))
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.__class__(super().__add__(other))
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self.__class__(super().__rmul__(other))
|
||||
|
24
scripts/docker_server/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
# Download base image debian jessie
|
||||
FROM python:slim
|
||||
|
||||
ARG replication_version=0.0.21
|
||||
ARG version=0.1.1
|
||||
|
||||
# Infos
|
||||
LABEL maintainer="Swann Martinez"
|
||||
LABEL version=$version
|
||||
LABEL description="Blender multi-user addon \
|
||||
dedicated server image."
|
||||
|
||||
# Argument
|
||||
ENV password='admin'
|
||||
ENV port=5555
|
||||
ENV timeout=3000
|
||||
ENV log_level=INFO
|
||||
ENV log_file="multiuser_server.log"
|
||||
|
||||
#Install replication
|
||||
RUN pip install replication==$replication_version
|
||||
|
||||
# Run the server with parameters
|
||||
CMD replication.serve -pwd ${password} -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}
|
6
scripts/get_addon_version.py
Normal file
@ -0,0 +1,6 @@
|
||||
import re
|
||||
|
||||
init_py = open("multi_user/__init__.py").read()
|
||||
version = re.search("\d+, \d+, \d+", init_py).group(0)
|
||||
digits = version.split(',')
|
||||
print('.'.join(digits).replace(" ",""))
|
4
scripts/get_replication_version.py
Normal file
@ -0,0 +1,4 @@
|
||||
import re
|
||||
|
||||
init_py = open("multi_user/__init__.py").read()
|
||||
print(re.search("\d+\.\d+\.\d+\w\d+|\d+\.\d+\.\d+", init_py).group(0))
|
10
scripts/start_server/run-dockerfile.sh
Normal 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
|
5
scripts/start_server/start-server.sh
Normal 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
|
@ -1,21 +0,0 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from deepdiff import DeepDiff
|
||||
|
||||
import bpy
|
||||
import random
|
||||
from multi_user.bl_types.bl_image import BlImage
|
||||
|
||||
def test_image(clear_blend):
|
||||
datablock = bpy.data.images.new('asd',2000,2000)
|
||||
|
||||
implementation = BlImage()
|
||||
expected = implementation._dump(datablock)
|
||||
bpy.data.images.remove(datablock)
|
||||
|
||||
test = implementation._construct(expected)
|
||||
implementation._load(expected, test)
|
||||
result = implementation._dump(test)
|
||||
|
||||
assert not DeepDiff(expected, result)
|
@ -7,13 +7,12 @@ import bpy
|
||||
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__()]
|
||||
|
||||
|
||||
datablock = bpy.data.materials.new("test")
|
||||
datablock.use_nodes = True
|
||||
bpy.data.materials.create_gpencil_data(datablock)
|
||||
|
||||
|
||||
for ntype in nodes_types:
|
||||
datablock.node_tree.nodes.new(ntype)
|
||||
|
||||
@ -26,3 +25,18 @@ def test_material(clear_blend):
|
||||
result = implementation._dump(test)
|
||||
|
||||
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)
|
||||
|
@ -6,8 +6,11 @@ from deepdiff import DeepDiff
|
||||
import bpy
|
||||
import random
|
||||
from multi_user.bl_types.bl_scene import BlScene
|
||||
from multi_user.utils import get_preferences
|
||||
|
||||
def test_scene(clear_blend):
|
||||
get_preferences().sync_flags.sync_render_settings = True
|
||||
|
||||
datablock = bpy.data.scenes.new("toto")
|
||||
datablock.view_settings.use_curve_mapping = True
|
||||
# Test
|
||||
|