Compare commits
69 Commits
datablock_
...
v0.1.0
Author | SHA1 | Date | |
---|---|---|---|
e0b56d8990 | |||
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,9 @@
|
||||
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,8 @@ build:
|
||||
image: debian:stable-slim
|
||||
script:
|
||||
- rm -rf tests .git .gitignore script
|
||||
|
||||
artifacts:
|
||||
name: multi_user
|
||||
paths:
|
||||
- multi_user
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
- develop
|
||||
|
||||
|
18
.gitlab/ci/deploy.gitlab-ci.yml
Normal file
18
.gitlab/ci/deploy.gitlab-ci.yml
Normal file
@ -0,0 +1,18 @@
|
||||
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}
|
@ -3,8 +3,3 @@ test:
|
||||
image: slumber/blender-addon-testing:latest
|
||||
script:
|
||||
- python3 scripts/test_addon.py
|
||||
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
- develop
|
||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@ -65,24 +65,34 @@ 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] - preview
|
||||
|
||||
### 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
|
49
README.md
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 |
|
||||
|
||||
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 8.4 KiB |
BIN
docs/getting_started/img/quickstart_advanced_cache.png
Normal file
BIN
docs/getting_started/img/quickstart_advanced_cache.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
BIN
docs/getting_started/img/quickstart_replication.png
Normal file
BIN
docs/getting_started/img/quickstart_replication.png
Normal file
Binary file not shown.
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
|
||||
---
|
||||
|
@ -19,7 +19,7 @@
|
||||
bl_info = {
|
||||
"name": "Multi-User",
|
||||
"author": "Swann Martinez",
|
||||
"version": (0, 0, 3),
|
||||
"version": (0, 1, 0),
|
||||
"description": "Enable real-time collaborative workflow inside blender",
|
||||
"blender": (2, 82, 0),
|
||||
"location": "3D View > Sidebar > Multi-User tab",
|
||||
@ -44,10 +44,12 @@ from . import environment, utils
|
||||
|
||||
|
||||
DEPENDENCIES = {
|
||||
("replication", '0.0.21a8'),
|
||||
("replication", '0.0.21a15'),
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
140
multi_user/bl_types/bl_file.py
Normal file
140
multi_user/bl_types/bl_file.py
Normal file
@ -0,0 +1,140 @@
|
||||
# ##### 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(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):
|
||||
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 = 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,9 +21,8 @@ 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*)\]')
|
||||
|
||||
|
@ -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,9 @@ 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
|
||||
|
||||
class BlScene(BlDatablock):
|
||||
bl_id = "scenes"
|
||||
@ -33,6 +35,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
|
||||
@ -53,22 +60,23 @@ class BlScene(BlDatablock):
|
||||
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.eevee, 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:
|
||||
#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,12 +88,14 @@ 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
|
||||
@ -97,10 +107,8 @@ class BlScene(BlDatablock):
|
||||
|
||||
scene_dumper.depth = 1
|
||||
scene_dumper.include_filter = None
|
||||
|
||||
pref = get_preferences()
|
||||
|
||||
if pref.sync_flags.sync_render_settings:
|
||||
if self.preferences.sync_flags.sync_render_settings:
|
||||
scene_dumper.exclude_filter = [
|
||||
'gi_cache_info',
|
||||
'feature_set',
|
||||
@ -114,7 +122,9 @@ class BlScene(BlDatablock):
|
||||
'preview_samples',
|
||||
'sample_clamp_indirect',
|
||||
'samples',
|
||||
'volume_bounces'
|
||||
'volume_bounces',
|
||||
'file_extension',
|
||||
'use_denoising'
|
||||
]
|
||||
data['eevee'] = scene_dumper.dump(instance.eevee)
|
||||
data['cycles'] = scene_dumper.dump(instance.cycles)
|
||||
@ -154,3 +164,17 @@ class BlScene(BlDatablock):
|
||||
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, cache_size=5000)
|
@ -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
|
||||
|
@ -37,6 +37,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
|
||||
@ -60,6 +63,7 @@ class BlWorld(BlDatablock):
|
||||
world_dumper.include_filter = [
|
||||
"use_nodes",
|
||||
"name",
|
||||
"color"
|
||||
]
|
||||
data = world_dumper.dump(instance)
|
||||
if instance.use_nodes:
|
||||
|
@ -19,7 +19,7 @@ import logging
|
||||
|
||||
import bpy
|
||||
|
||||
from . import operators, presence, utils
|
||||
from . import presence, utils
|
||||
from replication.constants import (FETCHED,
|
||||
UP,
|
||||
RP_COMMON,
|
||||
@ -31,10 +31,13 @@ from replication.constants import (FETCHED,
|
||||
STATE_SRV_SYNC,
|
||||
REPARENT)
|
||||
|
||||
from replication.interface import session
|
||||
|
||||
class Delayable():
|
||||
"""Delayable task interface
|
||||
"""
|
||||
def __init__(self):
|
||||
self.is_registered = False
|
||||
|
||||
def register(self):
|
||||
raise NotImplementedError
|
||||
@ -53,13 +56,20 @@ 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 +97,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, force=True)
|
||||
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, force=True)
|
||||
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 +130,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:
|
||||
@ -205,11 +213,16 @@ class DynamicRightSelectTimer(Timer):
|
||||
|
||||
class Draw(Delayable):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._handler = None
|
||||
|
||||
def register(self):
|
||||
self._handler = bpy.types.SpaceView3D.draw_handler_add(
|
||||
self.execute, (), 'WINDOW', 'POST_VIEW')
|
||||
if not self.is_registered:
|
||||
self._handler = bpy.types.SpaceView3D.draw_handler_add(
|
||||
self.execute, (), 'WINDOW', 'POST_VIEW')
|
||||
logging.debug(f"Register {self.__class__.__name__}")
|
||||
else:
|
||||
logging.debug(f"Drow {self.__class__.__name__} already registered")
|
||||
|
||||
def execute(self):
|
||||
raise NotImplementedError()
|
||||
@ -224,7 +237,6 @@ class Draw(Delayable):
|
||||
|
||||
class DrawClient(Draw):
|
||||
def execute(self):
|
||||
session = getattr(operators, 'client', None)
|
||||
renderer = getattr(presence, 'renderer', None)
|
||||
prefs = utils.get_preferences()
|
||||
|
||||
@ -260,22 +272,21 @@ 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']
|
||||
@ -330,12 +341,11 @@ class SessionUserSync(Timer):
|
||||
super().__init__(timout)
|
||||
|
||||
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):
|
||||
@ -350,3 +360,15 @@ class SessionUserSync(Timer):
|
||||
new_key = ui_users.add()
|
||||
new_key.name = user
|
||||
new_key.username = 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:
|
||||
|
@ -25,8 +25,9 @@ import string
|
||||
import time
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, Popen, TimeoutExpired
|
||||
import zmq
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
@ -38,17 +39,90 @@ from replication.constants import (FETCHED, STATE_ACTIVE,
|
||||
STATE_SYNCING, RP_COMMON, UP)
|
||||
from replication.data import ReplicatedDataFactory
|
||||
from replication.exception import NonAuthorizedOperationError
|
||||
from replication.interface import Session
|
||||
from replication.interface import session
|
||||
|
||||
|
||||
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 3: Launch presence overlay
|
||||
if runtime_settings.enable_presence:
|
||||
presence.renderer.run()
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
# Step 2: Unregister presence renderer
|
||||
presence.renderer.stop()
|
||||
|
||||
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 +135,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 +144,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 +178,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 +199,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 +216,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 +238,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,
|
||||
@ -183,56 +261,17 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
|
||||
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(
|
||||
@ -269,15 +308,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 +330,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:
|
||||
@ -319,11 +357,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))
|
||||
|
||||
@ -350,9 +388,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:
|
||||
@ -387,10 +424,9 @@ class SessionPropertyRightOperator(bpy.types.Operator):
|
||||
|
||||
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)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@ -437,10 +473,9 @@ class SessionSnapUserOperator(bpy.types.Operator):
|
||||
|
||||
if event.type == 'TIMER':
|
||||
area, region, rv3d = presence.view3d_find()
|
||||
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:
|
||||
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']
|
||||
@ -537,9 +570,7 @@ class SessionApply(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
global client
|
||||
|
||||
client.apply(self.target)
|
||||
session.apply(self.target)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@ -557,10 +588,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 +608,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 +637,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 +678,7 @@ classes = (
|
||||
ApplyArmatureOperator,
|
||||
SessionKickOperator,
|
||||
SessionInitOperator,
|
||||
|
||||
ClearCache,
|
||||
)
|
||||
|
||||
|
||||
@ -632,32 +690,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 +723,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
|
||||
@ -703,11 +757,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+')
|
||||
|
||||
@ -68,6 +72,16 @@ def update_port(self, context):
|
||||
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,44 @@ 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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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=True,
|
||||
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):
|
||||
@ -123,8 +170,8 @@ 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
|
||||
default=random.randrange(5570, 70000),
|
||||
update=update_port,
|
||||
)
|
||||
init_method: bpy.props.EnumProperty(
|
||||
name='init_method',
|
||||
@ -136,7 +183,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 +205,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
|
||||
@ -230,6 +278,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",
|
||||
@ -343,6 +397,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 +438,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:
|
||||
|
226
multi_user/ui.py
226
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
|
||||
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
|
||||
'TRIA_UP',
|
||||
'ERROR'] # CHANGED
|
||||
|
||||
|
||||
def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█', fill_empty=' '):
|
||||
@ -95,9 +96,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 +119,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 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 +166,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 +224,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 +255,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 +299,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 +337,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 +375,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 +406,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 +420,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 +429,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 +463,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,
|
||||
@ -485,48 +482,18 @@ class SESSION_PT_presence(bpy.types.Panel):
|
||||
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 +506,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(
|
||||
@ -576,7 +543,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 +552,6 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
session = operators.client
|
||||
settings = get_preferences()
|
||||
admin = False
|
||||
|
||||
@ -595,9 +560,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 +574,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 +599,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 +619,35 @@ 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()
|
||||
col.active = display_all
|
||||
|
||||
row = col.row(align=True)
|
||||
settings = context.window_manager.session
|
||||
layout.active = settings.enable_presence
|
||||
col = layout.column()
|
||||
col.prop(settings, "presence_show_selected")
|
||||
col.prop(settings, "presence_show_user")
|
||||
row = layout.column()
|
||||
row.active = settings.presence_show_user
|
||||
row.prop(settings, "presence_show_far_user")
|
||||
|
||||
classes = (
|
||||
SESSION_UL_users,
|
||||
@ -664,9 +657,8 @@ classes = (
|
||||
SESSION_PT_presence,
|
||||
SESSION_PT_advanced_settings,
|
||||
SESSION_PT_user,
|
||||
SESSION_PT_services,
|
||||
SESSION_PT_repository,
|
||||
|
||||
VIEW3D_PT_overlay_session,
|
||||
)
|
||||
|
||||
|
||||
|
@ -21,8 +21,10 @@ 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
|
||||
@ -47,7 +49,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 \
|
||||
@ -78,17 +80,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 +94,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
24
scripts/docker_server/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
# Download base image debian jessie
|
||||
FROM python:slim
|
||||
|
||||
ARG replication_version=0.0.21a15
|
||||
ARG version=0.1.0
|
||||
|
||||
# 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
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
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))
|
@ -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)
|
Reference in New Issue
Block a user